diff --git a/.gitignore b/.gitignore
index 5b4b102919c..3bb167e6a2f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,3 +33,4 @@ out/
local.properties
git.xml
build.info
+mobile/github-advrecyclerview_lib/bin
diff --git a/mobile/com.sap.sailing.android.shared/project.properties b/mobile/com.sap.sailing.android.shared/project.properties
index a0b67d7ac5e..0684bddb7cd 100755
--- a/mobile/com.sap.sailing.android.shared/project.properties
+++ b/mobile/com.sap.sailing.android.shared/project.properties
@@ -11,8 +11,8 @@
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
-target=android-21
+target=android-22
android.library=true
android.library.reference.1=../google-volley_lib
-android.library.reference.2=../google-android-support-v7-appcompat_lib
\ No newline at end of file
+android.library.reference.2=../google-android-support-v7-appcompat_lib
diff --git a/mobile/com.sap.sailing.android.tracking.app/project.properties b/mobile/com.sap.sailing.android.tracking.app/project.properties
index d922670b3d0..0f43975b9f0 100644
--- a/mobile/com.sap.sailing.android.tracking.app/project.properties
+++ b/mobile/com.sap.sailing.android.tracking.app/project.properties
@@ -11,6 +11,6 @@
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
-target=Google Inc.:Google APIs:21
+target=Google Inc.:Google APIs:22
android.library.reference.1=../google-play-services_lib
android.library.reference.2=../com.sap.sailing.android.shared
diff --git a/mobile/com.sap.sailing.racecommittee.app/project.properties b/mobile/com.sap.sailing.racecommittee.app/project.properties
index c62f3f61e53..e08b96b7c9d 100755
--- a/mobile/com.sap.sailing.racecommittee.app/project.properties
+++ b/mobile/com.sap.sailing.racecommittee.app/project.properties
@@ -11,7 +11,8 @@
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
-target=Google Inc.:Google APIs:21
+target=Google Inc.:Google APIs:22
android.library.reference.1=../google-play-services_lib
android.library.reference.2=../com.sap.sailing.android.shared
android.library.reference.3=../google-android-support-v7-recyclerview_lib
+android.library.reference.4=../github-advrecyclerview_lib
diff --git a/mobile/github-advrecyclerview_lib/.classpath b/mobile/github-advrecyclerview_lib/.classpath
new file mode 100644
index 00000000000..7bc01d9a9c6
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/.classpath
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/mobile/github-advrecyclerview_lib/.project b/mobile/github-advrecyclerview_lib/.project
new file mode 100644
index 00000000000..e376716905a
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/.project
@@ -0,0 +1,33 @@
+
+
+ com.h6ah4i.android.widget.advrecyclerview
+
+
+
+
+
+ com.android.ide.eclipse.adt.ResourceManagerBuilder
+
+
+
+
+ com.android.ide.eclipse.adt.PreCompilerBuilder
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ com.android.ide.eclipse.adt.ApkBuilder
+
+
+
+
+
+ com.android.ide.eclipse.adt.AndroidNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/mobile/github-advrecyclerview_lib/.settings/org.eclipse.jdt.core.prefs b/mobile/github-advrecyclerview_lib/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000000..7341ab1683c
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,11 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.7
diff --git a/mobile/github-advrecyclerview_lib/AndroidManifest.xml b/mobile/github-advrecyclerview_lib/AndroidManifest.xml
new file mode 100644
index 00000000000..80ca8cddac4
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/AndroidManifest.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/mobile/github-advrecyclerview_lib/lint.xml b/mobile/github-advrecyclerview_lib/lint.xml
new file mode 100644
index 00000000000..ee0eead5bb7
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/lint.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/mobile/github-advrecyclerview_lib/project.properties b/mobile/github-advrecyclerview_lib/project.properties
new file mode 100644
index 00000000000..eba516190a6
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/project.properties
@@ -0,0 +1,17 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-22
+android.library.reference.1=../google-android-support-v7-recyclerview_lib
+android.library=true
+android.library.reference.2=../google-android-support-v7-appcompat_lib
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/BaseItemAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/BaseItemAnimator.java
new file mode 100644
index 00000000000..0230e2d17f3
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/BaseItemAnimator.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.animator;
+
+import android.support.v7.widget.RecyclerView;
+
+public abstract class BaseItemAnimator extends RecyclerView.ItemAnimator {
+ private ItemAnimatorListener mListener;
+
+ /**
+ * The interface to be implemented by listeners to animation events from this
+ * ItemAnimator. This is used internally and is not intended for developers to
+ * create directly.
+ */
+ public interface ItemAnimatorListener {
+ void onRemoveFinished(RecyclerView.ViewHolder item);
+
+ void onAddFinished(RecyclerView.ViewHolder item);
+
+ void onMoveFinished(RecyclerView.ViewHolder item);
+
+ void onChangeFinished(RecyclerView.ViewHolder item);
+ }
+
+ /**
+ * Internal only:
+ * Sets the listener that must be called when the animator is finished
+ * animating the item (or immediately if no animation happens). This is set
+ * internally and is not intended to be set by external code.
+ *
+ * @param listener The listener that must be called.
+ */
+ public void setListener(ItemAnimatorListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public final void onAddStarting(RecyclerView.ViewHolder item) {
+ onAddStartingImpl(item);
+ }
+
+ @Override
+ public final void onAddFinished(RecyclerView.ViewHolder item) {
+ onAddFinishedImpl(item);
+
+ if (mListener != null) {
+ mListener.onAddFinished(item);
+ }
+ }
+
+ @Override
+ public final void onChangeStarting(RecyclerView.ViewHolder item, boolean oldItem) {
+ onChangeStartingItem(item, oldItem);
+ }
+
+ @Override
+ public final void onChangeFinished(RecyclerView.ViewHolder item, boolean oldItem) {
+ onChangeFinishedImpl(item, oldItem);
+
+ if (mListener != null) {
+ mListener.onChangeFinished(item);
+ }
+ }
+
+ @Override
+ public final void onMoveStarting(RecyclerView.ViewHolder item) {
+ onMoveStartingImpl(item);
+ }
+
+ @Override
+ public final void onMoveFinished(RecyclerView.ViewHolder item) {
+ onMoveFinishedImpl(item);
+
+ if (mListener != null) {
+ mListener.onMoveFinished(item);
+ }
+ }
+
+ @Override
+ public final void onRemoveStarting(RecyclerView.ViewHolder item) {
+ onRemoveStartingImpl(item);
+ }
+
+ @Override
+ public final void onRemoveFinished(RecyclerView.ViewHolder item) {
+ onRemoveFinishedImpl(item);
+
+ if (mListener != null) {
+ mListener.onRemoveFinished(item);
+ }
+ }
+
+ @SuppressWarnings("EmptyMethod")
+ protected void onAddStartingImpl(RecyclerView.ViewHolder item) {
+ }
+
+ @SuppressWarnings("EmptyMethod")
+ protected void onAddFinishedImpl(RecyclerView.ViewHolder item) {
+ }
+
+ @SuppressWarnings("EmptyMethod")
+ protected void onChangeStartingItem(RecyclerView.ViewHolder item, boolean oldItem) {
+ }
+
+ @SuppressWarnings("EmptyMethod")
+ protected void onChangeFinishedImpl(RecyclerView.ViewHolder item, boolean oldItem) {
+ }
+
+ @SuppressWarnings("EmptyMethod")
+ protected void onMoveStartingImpl(RecyclerView.ViewHolder item) {
+ }
+
+ @SuppressWarnings("EmptyMethod")
+ protected void onMoveFinishedImpl(RecyclerView.ViewHolder item) {
+ }
+
+ @SuppressWarnings("EmptyMethod")
+ protected void onRemoveStartingImpl(RecyclerView.ViewHolder item) {
+ }
+
+ @SuppressWarnings("EmptyMethod")
+ protected void onRemoveFinishedImpl(RecyclerView.ViewHolder item) {
+ }
+
+ public boolean dispatchFinishedWhenDone() {
+ if (!isRunning()) {
+ dispatchAnimationsFinished();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean debugLogEnabled() {
+ return false;
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/GeneralItemAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/GeneralItemAnimator.java
new file mode 100644
index 00000000000..f6f3aad8e2e
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/GeneralItemAnimator.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific langua
+ * ge governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.animator;
+
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemAddAnimationManager;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemChangeAnimationManager;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemMoveAnimationManager;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemRemoveAnimationManager;
+
+public abstract class GeneralItemAnimator extends BaseItemAnimator {
+ private static final String TAG = "ARVGeneralItemAnimator";
+
+ private boolean mDebug;
+
+ private ItemRemoveAnimationManager mRemoveAnimationManager;
+ private ItemAddAnimationManager mAddAnimationsManager;
+ private ItemChangeAnimationManager mChangeAnimationsManager;
+ private ItemMoveAnimationManager mMoveAnimationsManager;
+
+ GeneralItemAnimator() {
+ setup();
+ }
+
+ private void setup() {
+ onSetup();
+
+ if (mRemoveAnimationManager == null ||
+ mAddAnimationsManager == null ||
+ mChangeAnimationsManager == null ||
+ mMoveAnimationsManager == null) {
+ throw new IllegalStateException("setup incomplete");
+ }
+ }
+
+ protected abstract void onSetup();
+
+ @Override
+ public void runPendingAnimations() {
+ if (!hasPendingAnimations()) {
+ return;
+ }
+
+ onSchedulePendingAnimations();
+ }
+
+ @Override
+ public boolean animateRemove(final RecyclerView.ViewHolder holder) {
+ if (mDebug) {
+ Log.d(TAG, "animateRemove(id = " + holder.getItemId() + ", position = " + holder.getPosition() + ")");
+ }
+
+ return mRemoveAnimationManager.addPendingAnimation(holder);
+ }
+
+ @Override
+ public boolean animateAdd(final RecyclerView.ViewHolder holder) {
+ if (mDebug) {
+ Log.d(TAG, "animateAdd(id = " + holder.getItemId() + ", position = " + holder.getPosition() + ")");
+ }
+
+ return mAddAnimationsManager.addPendingAnimation(holder);
+ }
+
+ @Override
+ public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
+ if (mDebug) {
+ Log.d(TAG, "animateMove(id = " + holder.getItemId() + ", position = " + holder.getPosition() + ", fromX = " + fromX + ", fromY = " + fromY + ", toX = " + toX + ", toY = " + toY + ")");
+ }
+
+ return mMoveAnimationsManager.addPendingAnimation(holder, fromX, fromY, toX, toY);
+ }
+
+ @Override
+ public boolean animateChange(
+ RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY) {
+ if (mDebug) {
+ final String oldId = (oldHolder != null) ? Long.toString(oldHolder.getItemId()) : "-";
+ final String oldPosition = (oldHolder != null) ? Long.toString(oldHolder.getPosition()) : "-";
+ final String newId = (newHolder != null) ? Long.toString(newHolder.getItemId()) : "-";
+ final String newPosition = (newHolder != null) ? Long.toString(newHolder.getPosition()) : "-";
+
+ Log.d(TAG, "animateChange(old.id = " + oldId + ", old.position = " + oldPosition + ", new.id = " + newId + ", new.position = " + newPosition
+ + ", fromX = " + fromX + ", fromY = " + fromY + ", toX = " + toX + ", toY = " + toY + ")");
+ }
+
+ return mChangeAnimationsManager.addPendingAnimation(oldHolder, newHolder, fromX, fromY, toX, toY);
+ }
+
+ protected void cancelAnimations(RecyclerView.ViewHolder item) {
+ ViewCompat.animate(item.itemView).cancel();
+ }
+
+ @Override
+ public void endAnimation(RecyclerView.ViewHolder item) {
+
+ // this will trigger end callback which should set properties to their target values.
+ cancelAnimations(item);
+
+ mMoveAnimationsManager.endPendingAnimations(item);
+ mChangeAnimationsManager.endPendingAnimations(item);
+ mRemoveAnimationManager.endPendingAnimations(item);
+ mAddAnimationsManager.endPendingAnimations(item);
+
+ mMoveAnimationsManager.endDeferredReadyAnimations(item);
+ mChangeAnimationsManager.endDeferredReadyAnimations(item);
+ mRemoveAnimationManager.endDeferredReadyAnimations(item);
+ mAddAnimationsManager.endDeferredReadyAnimations(item);
+
+ // animations should be ended by the cancel above.
+ if (mRemoveAnimationManager.removeFromActive(item) && mDebug) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in the active animation list [remove]");
+ }
+
+ if (mAddAnimationsManager.removeFromActive(item) && mDebug) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in the active animation list [add]");
+ }
+
+ if (mChangeAnimationsManager.removeFromActive(item) && mDebug) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in the active animation list [change]");
+ }
+
+ if (mMoveAnimationsManager.removeFromActive(item) && mDebug) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in the active animation list [move]");
+ }
+
+ dispatchFinishedWhenDone();
+ }
+
+ @Override
+ public boolean isRunning() {
+ return (mRemoveAnimationManager.isRunning() ||
+ mAddAnimationsManager.isRunning() ||
+ mChangeAnimationsManager.isRunning() ||
+ mMoveAnimationsManager.isRunning());
+ }
+
+ @Override
+ public void endAnimations() {
+ // end all pending animations
+ mMoveAnimationsManager.endAllPendingAnimations();
+ mRemoveAnimationManager.endAllPendingAnimations();
+ mAddAnimationsManager.endAllPendingAnimations();
+ mChangeAnimationsManager.endAllPendingAnimations();
+
+ if (!isRunning()) {
+ return;
+ }
+
+ // end all deferred animations
+ mMoveAnimationsManager.endAllDeferredReadyAnimations();
+ mAddAnimationsManager.endAllDeferredReadyAnimations();
+ mChangeAnimationsManager.endAllDeferredReadyAnimations();
+
+ // cancel all started animations
+ mRemoveAnimationManager.cancelAllStartedAnimations();
+ mMoveAnimationsManager.cancelAllStartedAnimations();
+ mAddAnimationsManager.cancelAllStartedAnimations();
+ mChangeAnimationsManager.cancelAllStartedAnimations();
+
+ dispatchAnimationsFinished();
+ }
+
+ @Override
+ public boolean debugLogEnabled() {
+ return mDebug;
+ }
+
+ @Override
+ public boolean dispatchFinishedWhenDone() {
+ if (mDebug && !isRunning()) {
+ Log.d(TAG, "dispatchFinishedWhenDone()");
+ }
+
+ return super.dispatchFinishedWhenDone();
+ }
+
+ protected boolean hasPendingAnimations() {
+ return (mRemoveAnimationManager.hasPending() ||
+ mMoveAnimationsManager.hasPending() ||
+ mChangeAnimationsManager.hasPending() ||
+ mAddAnimationsManager.hasPending());
+ }
+
+ protected ItemRemoveAnimationManager getRemoveAnimationManager() {
+ return mRemoveAnimationManager;
+ }
+
+ protected void setItemRemoveAnimationManager(ItemRemoveAnimationManager removeAnimationManager) {
+ mRemoveAnimationManager = removeAnimationManager;
+ }
+
+ protected ItemAddAnimationManager getItemAddAnimationsManager() {
+ return mAddAnimationsManager;
+ }
+
+ protected void setItemAddAnimationsManager(ItemAddAnimationManager addAnimationsManager) {
+ mAddAnimationsManager = addAnimationsManager;
+ }
+
+ protected ItemChangeAnimationManager getItemChangeAnimationsManager() {
+ return mChangeAnimationsManager;
+ }
+
+ protected void setItemChangeAnimationsManager(ItemChangeAnimationManager changeAnimationsManager) {
+ mChangeAnimationsManager = changeAnimationsManager;
+ }
+
+ protected ItemMoveAnimationManager getItemMoveAnimationsManager() {
+ return mMoveAnimationsManager;
+ }
+
+ protected void setItemMoveAnimationsManager(ItemMoveAnimationManager moveAnimationsManager) {
+ mMoveAnimationsManager = moveAnimationsManager;
+ }
+
+ public boolean isDebug() {
+ return mDebug;
+ }
+
+ public void setDebug(boolean debug) {
+ mDebug = debug;
+ }
+
+ /**
+ * Schedule order and timing of pending animations.
+ * Override this method to custom animation order.
+ */
+ protected void onSchedulePendingAnimations() {
+ schedulePendingAnimationsByDefaultRule();
+ }
+
+ @SuppressWarnings("UnnecessaryLocalVariable")
+ protected void schedulePendingAnimationsByDefaultRule() {
+ final boolean removalsPending = mRemoveAnimationManager.hasPending();
+ final boolean movesPending = mMoveAnimationsManager.hasPending();
+ final boolean changesPending = mChangeAnimationsManager.hasPending();
+ final boolean additionsPending = mAddAnimationsManager.hasPending();
+
+ final long removeDuration = removalsPending ? getRemoveDuration() : 0;
+ final long moveDuration = movesPending ? getMoveDuration() : 0;
+ final long changeDuration = changesPending ? getChangeDuration() : 0;
+
+ if (removalsPending) {
+ mRemoveAnimationManager.runPendingAnimations(false, 0);
+ }
+
+ if (movesPending) {
+ final boolean deferred = removalsPending;
+ final long deferredDelay = removeDuration;
+ mMoveAnimationsManager.runPendingAnimations(deferred, deferredDelay);
+ }
+
+ if (changesPending) {
+ final boolean deferred = removalsPending;
+ final long deferredDelay = removeDuration;
+ mChangeAnimationsManager.runPendingAnimations(deferred, deferredDelay);
+ }
+
+ if (additionsPending) {
+ final boolean deferred = (removalsPending || movesPending || changesPending);
+ final long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
+ final long deferredDelay = (deferred) ? totalDelay : 0;
+ mAddAnimationsManager.runPendingAnimations(deferred, deferredDelay);
+ }
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/RefactoredDefaultItemAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/RefactoredDefaultItemAnimator.java
new file mode 100644
index 00000000000..fe6d8dbe6ca
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/RefactoredDefaultItemAnimator.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.animator;
+
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.ViewPropertyAnimatorCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.AddAnimationInfo;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ChangeAnimationInfo;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemAddAnimationManager;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemChangeAnimationManager;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemMoveAnimationManager;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemRemoveAnimationManager;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.MoveAnimationInfo;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.RemoveAnimationInfo;
+
+public class RefactoredDefaultItemAnimator extends GeneralItemAnimator {
+
+ @Override
+ protected void onSetup() {
+ setItemAddAnimationsManager(new DefaultItemAddAnimationManager(this));
+ setItemRemoveAnimationManager(new DefaultItemRemoveAnimationManager(this));
+ setItemChangeAnimationsManager(new DefaultItemChangeAnimationManager(this));
+ setItemMoveAnimationsManager(new DefaultItemMoveAnimationManager(this));
+ }
+
+ @Override
+ protected void onSchedulePendingAnimations() {
+ schedulePendingAnimationsByDefaultRule();
+ }
+
+ /**
+ * Item Animation manager for ADD operation (Same behavior as DefaultItemAnimator class)
+ */
+ private static class DefaultItemAddAnimationManager extends ItemAddAnimationManager {
+
+ public DefaultItemAddAnimationManager(BaseItemAnimator itemAnimator) {
+ super(itemAnimator);
+ }
+
+ @Override
+ protected void onCreateAnimation(AddAnimationInfo info) {
+ final ViewPropertyAnimatorCompat animator = ViewCompat.animate(info.holder.itemView);
+
+ animator.alpha(1);
+ animator.setDuration(getDuration());
+
+ startActiveItemAnimation(info, info.holder, animator);
+ }
+
+ @Override
+ protected void onAnimationEndedSuccessfully(AddAnimationInfo info, RecyclerView.ViewHolder item) {
+ }
+
+ @Override
+ protected void onAnimationEndedBeforeStarted(AddAnimationInfo info, RecyclerView.ViewHolder item) {
+ ViewCompat.setAlpha(item.itemView, 1);
+ }
+
+ @Override
+ protected void onAnimationCancel(AddAnimationInfo info, RecyclerView.ViewHolder item) {
+ ViewCompat.setAlpha(item.itemView, 1);
+ }
+
+ @Override
+ public boolean addPendingAnimation(RecyclerView.ViewHolder item) {
+ endAnimation(item);
+
+ ViewCompat.setAlpha(item.itemView, 0);
+
+ enqueuePendingAnimationInfo(new AddAnimationInfo(item));
+
+ return true;
+ }
+ }
+
+ /**
+ * Item Animation manager for REMOVE operation (Same behavior as DefaultItemAnimator class)
+ */
+ private static class DefaultItemRemoveAnimationManager extends ItemRemoveAnimationManager {
+
+ public DefaultItemRemoveAnimationManager(BaseItemAnimator itemAnimator) {
+ super(itemAnimator);
+ }
+
+ @Override
+ protected void onCreateAnimation(RemoveAnimationInfo info) {
+ final ViewPropertyAnimatorCompat animator = ViewCompat.animate(info.holder.itemView);
+
+ animator.setDuration(getDuration());
+ animator.alpha(0);
+
+ startActiveItemAnimation(info, info.holder, animator);
+ }
+
+ @Override
+ protected void onAnimationEndedSuccessfully(RemoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ final View view = item.itemView;
+ ViewCompat.setAlpha(view, 1);
+ }
+
+ @Override
+ protected void onAnimationEndedBeforeStarted(RemoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ final View view = item.itemView;
+ ViewCompat.setAlpha(view, 1);
+ }
+
+ @Override
+ protected void onAnimationCancel(RemoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ }
+
+ @Override
+ public boolean addPendingAnimation(RecyclerView.ViewHolder holder) {
+ endAnimation(holder);
+
+ enqueuePendingAnimationInfo(new RemoveAnimationInfo(holder));
+ return true;
+ }
+ }
+
+ /**
+ * Item Animation manager for CHANGE operation (Same behavior as DefaultItemAnimator class)
+ */
+ private static class DefaultItemChangeAnimationManager extends ItemChangeAnimationManager {
+ public DefaultItemChangeAnimationManager(BaseItemAnimator itemAnimator) {
+ super(itemAnimator);
+ }
+
+ @Override
+ protected void onCreateChangeAnimationForOldItem(ChangeAnimationInfo info) {
+ final ViewPropertyAnimatorCompat animator = ViewCompat.animate(info.oldHolder.itemView);
+
+ animator.setDuration(getDuration());
+ animator.translationX(info.toX - info.fromX);
+ animator.translationY(info.toY - info.fromY);
+ animator.alpha(0);
+
+ startActiveItemAnimation(info, info.oldHolder, animator);
+ }
+
+
+ @Override
+ protected void onCreateChangeAnimationForNewItem(ChangeAnimationInfo info) {
+ final ViewPropertyAnimatorCompat animator = ViewCompat.animate(info.newHolder.itemView);
+
+ animator.translationX(0);
+ animator.translationY(0);
+ animator.setDuration(getDuration());
+ animator.alpha(1);
+
+ startActiveItemAnimation(info, info.newHolder, animator);
+ }
+
+ @Override
+ protected void onAnimationEndedSuccessfully(ChangeAnimationInfo info, RecyclerView.ViewHolder item) {
+ final View view = item.itemView;
+ ViewCompat.setAlpha(view, 1);
+ ViewCompat.setTranslationX(view, 0);
+ ViewCompat.setTranslationY(view, 0);
+ }
+
+ @Override
+ protected void onAnimationEndedBeforeStarted(ChangeAnimationInfo info, RecyclerView.ViewHolder item) {
+ final View view = item.itemView;
+ ViewCompat.setAlpha(view, 1);
+ ViewCompat.setTranslationX(view, 0);
+ ViewCompat.setTranslationY(view, 0);
+ }
+
+ @Override
+ protected void onAnimationCancel(ChangeAnimationInfo info, RecyclerView.ViewHolder item) {
+ }
+
+ @Override
+ public boolean addPendingAnimation(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);
+
+ final int deltaX = (int) (toX - fromX - prevTranslationX);
+ final 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);
+ }
+
+ enqueuePendingAnimationInfo(new ChangeAnimationInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
+
+ return true;
+ }
+ }
+
+ /**
+ * Item Animation manager for MOVE operation (Same behavior as DefaultItemAnimator class)
+ */
+ private static class DefaultItemMoveAnimationManager extends ItemMoveAnimationManager {
+
+ public DefaultItemMoveAnimationManager(BaseItemAnimator itemAnimator) {
+ super(itemAnimator);
+ }
+
+ @Override
+ protected void onCreateAnimation(MoveAnimationInfo info) {
+ final View view = info.holder.itemView;
+ final int deltaX = info.toX - info.fromX;
+ final int deltaY = info.toY - info.fromY;
+
+ if (deltaX != 0) {
+ ViewCompat.animate(view).translationX(0);
+ }
+ if (deltaY != 0) {
+ ViewCompat.animate(view).translationY(0);
+ }
+
+ final ViewPropertyAnimatorCompat animator = ViewCompat.animate(view);
+
+ animator.setDuration(getDuration());
+
+ startActiveItemAnimation(info, info.holder, animator);
+ }
+
+ @Override
+ protected void onAnimationEndedSuccessfully(MoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ }
+
+ @Override
+ protected void onAnimationEndedBeforeStarted(MoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ final View view = item.itemView;
+ ViewCompat.setTranslationY(view, 0);
+ ViewCompat.setTranslationX(view, 0);
+ }
+
+ @Override
+ protected void onAnimationCancel(MoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ final View view = item.itemView;
+ final int deltaX = info.toX - info.fromX;
+ final int deltaY = info.toY - info.fromY;
+
+ if (deltaX != 0) {
+ ViewCompat.animate(view).translationX(0);
+ }
+ if (deltaY != 0) {
+ ViewCompat.animate(view).translationY(0);
+ }
+
+ if (deltaX != 0) {
+ ViewCompat.setTranslationX(view, 0);
+ }
+ if (deltaY != 0) {
+ ViewCompat.setTranslationY(view, 0);
+ }
+ }
+
+ @Override
+ public boolean addPendingAnimation(RecyclerView.ViewHolder item, int fromX, int fromY, int toX, int toY) {
+ final View view = item.itemView;
+
+ fromX += ViewCompat.getTranslationX(item.itemView);
+ fromY += ViewCompat.getTranslationY(item.itemView);
+
+ endAnimation(item);
+
+ final int deltaX = toX - fromX;
+ final int deltaY = toY - fromY;
+
+ final MoveAnimationInfo info = new MoveAnimationInfo(item, fromX, fromY, toX, toY);
+
+ if (deltaX == 0 && deltaY == 0) {
+ dispatchFinished(info, info.holder);
+ info.clear(info.holder);
+ return false;
+ }
+
+ if (deltaX != 0) {
+ ViewCompat.setTranslationX(view, -deltaX);
+ }
+ if (deltaY != 0) {
+ ViewCompat.setTranslationY(view, -deltaY);
+ }
+
+ enqueuePendingAnimationInfo(info);
+
+ return true;
+ }
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/SwipeDismissItemAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/SwipeDismissItemAnimator.java
new file mode 100644
index 00000000000..6bf1514325d
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/SwipeDismissItemAnimator.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.animator;
+
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.ViewPropertyAnimatorCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.AddAnimationInfo;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ChangeAnimationInfo;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemAddAnimationManager;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemChangeAnimationManager;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemMoveAnimationManager;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.ItemRemoveAnimationManager;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.MoveAnimationInfo;
+import com.h6ah4i.android.widget.advrecyclerview.animator.impl.RemoveAnimationInfo;
+import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager;
+import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemViewHolder;
+
+public class SwipeDismissItemAnimator extends GeneralItemAnimator {
+
+ public static final Interpolator MOVE_INTERPOLATOR = new AccelerateDecelerateInterpolator();
+
+ @Override
+ protected void onSetup() {
+ setItemAddAnimationsManager(new DefaultItemAddAnimationManager(this));
+ setItemRemoveAnimationManager(new SwipeDismissItemRemoveAnimationManager(this));
+ setItemChangeAnimationsManager(new SwipeDismissItemChangeAnimationManager(this));
+ setItemMoveAnimationsManager(new SwipeDismissItemMoveAnimationManager(this));
+
+ setRemoveDuration(150);
+ setMoveDuration(150);
+ }
+
+ @Override
+ protected void onSchedulePendingAnimations() {
+ schedulePendingAnimationsByDefaultRule();
+ }
+
+ @Override
+ protected void cancelAnimations(RecyclerView.ViewHolder item) {
+ super.cancelAnimations(item);
+
+// if (item instanceof SwipeableItemViewHolder) {
+// final View containerView = ((SwipeableItemViewHolder) item).getSwipeableContainerView();
+// ViewCompat.animate(containerView).cancel();
+// }
+ }
+
+ /**
+ * Item Animation manager for ADD operation (Same behavior as DefaultItemAnimator class)
+ */
+ private static class DefaultItemAddAnimationManager extends ItemAddAnimationManager {
+
+ public DefaultItemAddAnimationManager(BaseItemAnimator itemAnimator) {
+ super(itemAnimator);
+ }
+
+ @Override
+ protected void onCreateAnimation(AddAnimationInfo info) {
+ final ViewPropertyAnimatorCompat animator = ViewCompat.animate(info.holder.itemView);
+
+ animator.alpha(1);
+ animator.setDuration(getDuration());
+
+ startActiveItemAnimation(info, info.holder, animator);
+ }
+
+ @Override
+ protected void onAnimationEndedSuccessfully(AddAnimationInfo info, RecyclerView.ViewHolder item) {
+ }
+
+ @Override
+ protected void onAnimationEndedBeforeStarted(AddAnimationInfo info, RecyclerView.ViewHolder item) {
+ ViewCompat.setAlpha(item.itemView, 1);
+ }
+
+ @Override
+ protected void onAnimationCancel(AddAnimationInfo info, RecyclerView.ViewHolder item) {
+ ViewCompat.setAlpha(item.itemView, 1);
+ }
+
+ @Override
+ public boolean addPendingAnimation(RecyclerView.ViewHolder item) {
+ endAnimation(item);
+
+ ViewCompat.setAlpha(item.itemView, 0);
+
+ enqueuePendingAnimationInfo(new AddAnimationInfo(item));
+
+ return true;
+ }
+ }
+
+ /**
+ * Item Animation manager for REMOVE operation (Same behavior as DefaultItemAnimator class)
+ */
+ private static class SwipeDismissItemRemoveAnimationManager extends ItemRemoveAnimationManager {
+ private static final Interpolator DEFAULT_INTERPOLATOR = new AccelerateDecelerateInterpolator();
+
+ public SwipeDismissItemRemoveAnimationManager(BaseItemAnimator itemAnimator) {
+ super(itemAnimator);
+ }
+
+ @Override
+ protected void onCreateAnimation(RemoveAnimationInfo info) {
+ final ViewPropertyAnimatorCompat animator;
+
+ if (isSwipeDismissed(info.holder)) {
+ final View view = info.holder.itemView;
+ animator = ViewCompat.animate(view);
+ animator.setDuration(getDuration());
+ } else {
+ final View view = info.holder.itemView;
+ animator = ViewCompat.animate(view);
+ animator.setDuration(getDuration());
+ animator.setInterpolator(DEFAULT_INTERPOLATOR);
+ animator.alpha(0);
+ }
+
+ startActiveItemAnimation(info, info.holder, animator);
+ }
+
+ private static boolean isSwipeDismissed(RecyclerView.ViewHolder item) {
+ if (!(item instanceof SwipeableItemViewHolder)) {
+ return false;
+ }
+
+ final SwipeableItemViewHolder item2 = (SwipeableItemViewHolder) item;
+ final int result = item2.getSwipeResult();
+ final int reaction = item2.getAfterSwipeReaction();
+
+ return ((result == RecyclerViewSwipeManager.RESULT_SWIPED_LEFT) ||
+ (result == RecyclerViewSwipeManager.RESULT_SWIPED_RIGHT)) &&
+ (reaction == RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_REMOVE_ITEM);
+ }
+
+ private static boolean isSwipeDismissed(RemoveAnimationInfo info) {
+ return (info instanceof SwipeDismissRemoveAnimationInfo);
+ }
+
+ @Override
+ protected void onAnimationEndedSuccessfully(RemoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ final View view = item.itemView;
+
+ if (isSwipeDismissed(info)) {
+ ViewCompat.setTranslationX(view, 0);
+ ViewCompat.setTranslationY(view, 0);
+ } else {
+ ViewCompat.setAlpha(view, 1);
+ }
+ }
+
+ @Override
+ protected void onAnimationEndedBeforeStarted(RemoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ final View view = item.itemView;
+
+ if (isSwipeDismissed(info)) {
+ ViewCompat.setTranslationX(view, 0);
+ ViewCompat.setTranslationY(view, 0);
+ } else {
+ ViewCompat.setAlpha(view, 1);
+ }
+ }
+
+ @Override
+ protected void onAnimationCancel(RemoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ }
+
+ @Override
+ public boolean addPendingAnimation(RecyclerView.ViewHolder holder) {
+ if (isSwipeDismissed(holder)) {
+ final View itemView = holder.itemView;
+ final int prevItemX = (int) (ViewCompat.getTranslationX(itemView) + 0.5f);
+ final int prevItemY = (int) (ViewCompat.getTranslationY(itemView) + 0.5f);
+
+ endAnimation(holder);
+
+ ViewCompat.setTranslationX(itemView, prevItemX);
+ ViewCompat.setTranslationY(itemView, prevItemY);
+
+ enqueuePendingAnimationInfo(new SwipeDismissRemoveAnimationInfo(holder));
+
+ return true;
+ } else {
+ endAnimation(holder);
+
+ enqueuePendingAnimationInfo(new RemoveAnimationInfo(holder));
+
+ return true;
+ }
+ }
+ }
+
+ private static class SwipeDismissRemoveAnimationInfo extends RemoveAnimationInfo {
+ public SwipeDismissRemoveAnimationInfo(RecyclerView.ViewHolder holder) {
+ super(holder);
+ }
+ }
+
+ /**
+ * Item Animation manager for CHANGE operation (Same behavior as DefaultItemAnimator class)
+ */
+ private static class SwipeDismissItemChangeAnimationManager extends ItemChangeAnimationManager {
+ public SwipeDismissItemChangeAnimationManager(BaseItemAnimator itemAnimator) {
+ super(itemAnimator);
+ }
+
+ @Override
+ protected void onCreateChangeAnimationForOldItem(ChangeAnimationInfo info) {
+ final ViewPropertyAnimatorCompat animator = ViewCompat.animate(info.oldHolder.itemView);
+
+ animator.setDuration(getDuration());
+ animator.translationX(info.toX - info.fromX);
+ animator.translationY(info.toY - info.fromY);
+ animator.alpha(0);
+
+ startActiveItemAnimation(info, info.oldHolder, animator);
+ }
+
+
+ @Override
+ protected void onCreateChangeAnimationForNewItem(ChangeAnimationInfo info) {
+ final ViewPropertyAnimatorCompat animator = ViewCompat.animate(info.newHolder.itemView);
+
+ animator.translationX(0);
+ animator.translationY(0);
+ animator.setDuration(getDuration());
+ animator.alpha(1);
+
+ startActiveItemAnimation(info, info.newHolder, animator);
+ }
+
+ @Override
+ protected void onAnimationEndedSuccessfully(ChangeAnimationInfo info, RecyclerView.ViewHolder item) {
+ final View view = item.itemView;
+ ViewCompat.setAlpha(view, 1);
+ ViewCompat.setTranslationX(view, 0);
+ ViewCompat.setTranslationY(view, 0);
+ }
+
+ @Override
+ protected void onAnimationEndedBeforeStarted(ChangeAnimationInfo info, RecyclerView.ViewHolder item) {
+ final View view = item.itemView;
+ ViewCompat.setAlpha(view, 1);
+ ViewCompat.setTranslationX(view, 0);
+ ViewCompat.setTranslationY(view, 0);
+ }
+
+ @Override
+ protected void onAnimationCancel(ChangeAnimationInfo info, RecyclerView.ViewHolder item) {
+ }
+
+ @Override
+ public boolean addPendingAnimation(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);
+
+ final int deltaX = (int) (toX - fromX - prevTranslationX);
+ final 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);
+ }
+
+ enqueuePendingAnimationInfo(new ChangeAnimationInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
+
+ return true;
+ }
+ }
+
+ /**
+ * Item Animation manager for MOVE operation (Same behavior as DefaultItemAnimator class)
+ */
+ private static class SwipeDismissItemMoveAnimationManager extends ItemMoveAnimationManager {
+
+ public SwipeDismissItemMoveAnimationManager(BaseItemAnimator itemAnimator) {
+ super(itemAnimator);
+ }
+
+ @Override
+ protected void onCreateAnimation(MoveAnimationInfo info) {
+ final View view = info.holder.itemView;
+ final int deltaX = info.toX - info.fromX;
+ final int deltaY = info.toY - info.fromY;
+
+ if (deltaX != 0) {
+ ViewCompat.animate(view).translationX(0);
+ }
+ if (deltaY != 0) {
+ ViewCompat.animate(view).translationY(0);
+ }
+
+ final ViewPropertyAnimatorCompat animator = ViewCompat.animate(view);
+
+ animator.setDuration(getDuration());
+ animator.setInterpolator(MOVE_INTERPOLATOR);
+
+ startActiveItemAnimation(info, info.holder, animator);
+ }
+
+ @Override
+ protected void onAnimationEndedSuccessfully(MoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ }
+
+ @Override
+ protected void onAnimationEndedBeforeStarted(MoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ final View view = item.itemView;
+ ViewCompat.setTranslationY(view, 0);
+ ViewCompat.setTranslationX(view, 0);
+ }
+
+ @Override
+ protected void onAnimationCancel(MoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ final View view = item.itemView;
+ final int deltaX = info.toX - info.fromX;
+ final int deltaY = info.toY - info.fromY;
+
+ if (deltaX != 0) {
+ ViewCompat.animate(view).translationX(0);
+ }
+ if (deltaY != 0) {
+ ViewCompat.animate(view).translationY(0);
+ }
+
+ if (deltaX != 0) {
+ ViewCompat.setTranslationX(view, 0);
+ }
+ if (deltaY != 0) {
+ ViewCompat.setTranslationY(view, 0);
+ }
+ }
+
+ @Override
+ public boolean addPendingAnimation(RecyclerView.ViewHolder item, int fromX, int fromY, int toX, int toY) {
+ final View view = item.itemView;
+
+ fromX += ViewCompat.getTranslationX(item.itemView);
+ fromY += ViewCompat.getTranslationY(item.itemView);
+
+ endAnimation(item);
+
+ final int deltaX = toX - fromX;
+ final int deltaY = toY - fromY;
+
+ final MoveAnimationInfo info = new MoveAnimationInfo(item, fromX, fromY, toX, toY);
+
+ if (deltaX == 0 && deltaY == 0) {
+ dispatchFinished(info, info.holder);
+ info.clear(info.holder);
+ return false;
+ }
+
+ if (deltaX != 0) {
+ ViewCompat.setTranslationX(view, -deltaX);
+ }
+ if (deltaY != 0) {
+ ViewCompat.setTranslationY(view, -deltaY);
+ }
+
+ enqueuePendingAnimationInfo(info);
+
+ return true;
+ }
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/AddAnimationInfo.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/AddAnimationInfo.java
new file mode 100644
index 00000000000..faf644a0e62
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/AddAnimationInfo.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.animator.impl;
+
+import android.support.v7.widget.RecyclerView;
+
+public class AddAnimationInfo extends ItemAnimationInfo {
+ public RecyclerView.ViewHolder holder;
+
+ public AddAnimationInfo(RecyclerView.ViewHolder holder) {
+ this.holder = holder;
+ }
+
+ @Override
+ public RecyclerView.ViewHolder getAvailableViewHolder() {
+ return holder;
+ }
+
+ @Override
+ public void clear(RecyclerView.ViewHolder item) {
+ if (holder == null) {
+ holder = null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "AddAnimationInfo{" +
+ "holder=" + holder +
+ '}';
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/BaseItemAnimationManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/BaseItemAnimationManager.java
new file mode 100644
index 00000000000..86c4050957d
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/BaseItemAnimationManager.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.animator.impl;
+
+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 com.h6ah4i.android.widget.advrecyclerview.animator.BaseItemAnimator;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class BaseItemAnimationManager {
+ protected BaseItemAnimator mItemAnimator;
+ protected List mPending;
+ protected List> mDeferredReadySets;
+ protected List mActive;
+
+ public BaseItemAnimationManager(BaseItemAnimator itemAnimator) {
+ mItemAnimator = itemAnimator;
+ mPending = new ArrayList<>();
+ mActive = new ArrayList<>();
+ mDeferredReadySets = new ArrayList<>();
+ }
+
+ protected final boolean debugLogEnabled() {
+ return mItemAnimator.debugLogEnabled();
+ }
+
+ public boolean hasPending() {
+ return !mPending.isEmpty();
+ }
+
+ public boolean isRunning() {
+ return !mPending.isEmpty() || !mActive.isEmpty() || !mDeferredReadySets.isEmpty();
+ }
+
+ public boolean removeFromActive(RecyclerView.ViewHolder item) {
+ return mActive.remove(item);
+ }
+
+ public void cancelAllStartedAnimations() {
+ final List active = mActive;
+ for (int i = active.size() - 1; i >= 0; i--) {
+ final View view = active.get(i).itemView;
+ ViewCompat.animate(view).cancel();
+ }
+ }
+
+ public void runPendingAnimations(boolean deferred, long deferredDelay) {
+ final List ready = new ArrayList<>();
+
+ ready.addAll(mPending);
+ mPending.clear();
+
+ if (deferred) {
+ mDeferredReadySets.add(ready);
+
+ final Runnable process = new Runnable() {
+ @Override
+ public void run() {
+ for (T info : ready) {
+ createAnimation(info);
+ }
+ ready.clear();
+ mDeferredReadySets.remove(ready);
+ }
+ };
+
+ final View view = ready.get(0).getAvailableViewHolder().itemView;
+ ViewCompat.postOnAnimationDelayed(view, process, deferredDelay);
+ } else {
+ for (T info : ready) {
+ createAnimation(info);
+ }
+ ready.clear();
+ }
+ }
+
+ public abstract void dispatchStarting(T info, RecyclerView.ViewHolder item);
+
+ public abstract void dispatchFinished(T info, RecyclerView.ViewHolder item);
+
+ public abstract long getDuration();
+
+ public abstract void setDuration(long duration);
+
+ public void endPendingAnimations(RecyclerView.ViewHolder item) {
+ final List pending = mPending;
+
+ for (int i = pending.size() - 1; i >= 0; i--) {
+ final T info = pending.get(i);
+
+ if (endNotStartedAnimation(info, item) && (item != null)) {
+ pending.remove(i);
+ }
+ }
+
+ if (item == null) {
+ pending.clear();
+ }
+ }
+
+ public void endAllPendingAnimations() {
+ endPendingAnimations(null);
+ }
+
+ public void endDeferredReadyAnimations(RecyclerView.ViewHolder item) {
+ for (int i = mDeferredReadySets.size() - 1; i >= 0; i--) {
+ final List ready = mDeferredReadySets.get(i);
+
+ for (int j = ready.size() - 1; j >= 0; j--) {
+ final T info = ready.get(j);
+
+ if (endNotStartedAnimation(info, item) && (item != null)) {
+ ready.remove(j);
+ }
+ }
+
+ if (item == null) {
+ ready.clear();
+ }
+
+ if (ready.isEmpty()) {
+ mDeferredReadySets.remove(ready);
+ }
+ }
+ }
+
+ public void endAllDeferredReadyAnimations() {
+ endDeferredReadyAnimations(null);
+ }
+
+ /*package*/ void createAnimation(T info) {
+ onCreateAnimation(info);
+ }
+
+ protected void endAnimation(RecyclerView.ViewHolder holder) {
+ mItemAnimator.endAnimation(holder);
+ }
+
+ protected void dispatchFinishedWhenDone() {
+ mItemAnimator.dispatchFinishedWhenDone();
+ }
+
+ protected void enqueuePendingAnimationInfo(T info) {
+ if (info == null) {
+ throw new IllegalStateException("info is null");
+ }
+ mPending.add(info);
+ }
+
+ protected void startActiveItemAnimation(T info, RecyclerView.ViewHolder holder, ViewPropertyAnimatorCompat animator) {
+ animator.setListener(new BaseAnimatorListener(this, info, holder, animator));
+ addActiveAnimationTarget(holder);
+ animator.start();
+ }
+
+ private void addActiveAnimationTarget(RecyclerView.ViewHolder item) {
+ if (item == null) {
+ throw new IllegalStateException("item is null");
+ }
+ mActive.add(item);
+ }
+
+ protected abstract void onCreateAnimation(final T info);
+
+ protected abstract void onAnimationEndedSuccessfully(T info, RecyclerView.ViewHolder item);
+
+ protected abstract void onAnimationEndedBeforeStarted(T info, RecyclerView.ViewHolder item);
+
+ protected abstract void onAnimationCancel(T info, RecyclerView.ViewHolder item);
+
+ protected abstract boolean endNotStartedAnimation(T info, RecyclerView.ViewHolder item);
+
+
+ protected static class BaseAnimatorListener implements ViewPropertyAnimatorListener {
+ private BaseItemAnimationManager mManager;
+ private ItemAnimationInfo mAnimationInfo;
+ private RecyclerView.ViewHolder mHolder;
+ private ViewPropertyAnimatorCompat mAnimator;
+
+ public BaseAnimatorListener(BaseItemAnimationManager manager, ItemAnimationInfo info, RecyclerView.ViewHolder holder, ViewPropertyAnimatorCompat animator) {
+ mManager = manager;
+ mAnimationInfo = info;
+ mHolder = holder;
+ mAnimator = animator;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onAnimationStart(View view) {
+ mManager.dispatchStarting(mAnimationInfo, mHolder);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onAnimationEnd(View view) {
+ final BaseItemAnimationManager manager = mManager;
+ final ItemAnimationInfo info = mAnimationInfo;
+ final RecyclerView.ViewHolder holder = mHolder;
+
+ mAnimator.setListener(null);
+ mManager = null;
+ mAnimationInfo = null;
+ mHolder = null;
+ mAnimator = null;
+
+ manager.onAnimationEndedSuccessfully(info, holder);
+ manager.dispatchFinished(info, holder);
+ info.clear(holder);
+ manager.mActive.remove(holder);
+ manager.dispatchFinishedWhenDone();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onAnimationCancel(View view) {
+ mManager.onAnimationCancel(mAnimationInfo, mHolder);
+ }
+ }
+}
+
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ChangeAnimationInfo.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ChangeAnimationInfo.java
new file mode 100644
index 00000000000..dd8931a5578
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ChangeAnimationInfo.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.animator.impl;
+
+import android.support.v7.widget.RecyclerView;
+
+public class ChangeAnimationInfo extends ItemAnimationInfo {
+ public RecyclerView.ViewHolder newHolder, oldHolder;
+ public int fromX, fromY, toX, toY;
+
+ public ChangeAnimationInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
+ int fromX, int fromY, int toX, int toY) {
+ this.oldHolder = oldHolder;
+ this.newHolder = newHolder;
+ this.fromX = fromX;
+ this.fromY = fromY;
+ this.toX = toX;
+ this.toY = toY;
+ }
+
+ @Override
+ public RecyclerView.ViewHolder getAvailableViewHolder() {
+ return (oldHolder != null) ? oldHolder : newHolder;
+ }
+
+ @Override
+ public void clear(RecyclerView.ViewHolder item) {
+ if (oldHolder == item) {
+ oldHolder = null;
+ }
+ if (newHolder == item) {
+ newHolder = null;
+ }
+ if (oldHolder == null && newHolder == null) {
+ fromX = 0;
+ fromY = 0;
+ toX = 0;
+ toY = 0;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ChangeInfo{" +
+ ", oldHolder=" + oldHolder +
+ ", newHolder=" + newHolder +
+ ", fromX=" + fromX +
+ ", fromY=" + fromY +
+ ", toX=" + toX +
+ ", toY=" + toY +
+ '}';
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAddAnimationManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAddAnimationManager.java
new file mode 100644
index 00000000000..8e514f07c46
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAddAnimationManager.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.animator.impl;
+
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+
+import com.h6ah4i.android.widget.advrecyclerview.animator.BaseItemAnimator;
+
+public abstract class ItemAddAnimationManager extends BaseItemAnimationManager {
+ private static final String TAG = "ARVItemAddAnimMgr";
+
+ public ItemAddAnimationManager(BaseItemAnimator itemAnimator) {
+ super(itemAnimator);
+ }
+
+ @Override
+ public long getDuration() {
+ return mItemAnimator.getAddDuration();
+ }
+
+ @Override
+ public void setDuration(long duration) {
+ mItemAnimator.setAddDuration(duration);
+ }
+
+ @Override
+ public void dispatchStarting(AddAnimationInfo info, RecyclerView.ViewHolder item) {
+ if (debugLogEnabled()) {
+ Log.d(TAG, "dispatchAddStarting(" + item + ")");
+ }
+ mItemAnimator.dispatchAddStarting(item);
+ }
+
+ @Override
+ public void dispatchFinished(AddAnimationInfo info, RecyclerView.ViewHolder item) {
+ if (debugLogEnabled()) {
+ Log.d(TAG, "dispatchAddFinished(" + item + ")");
+ }
+ mItemAnimator.dispatchAddFinished(item);
+ }
+
+ @Override
+ protected boolean endNotStartedAnimation(AddAnimationInfo info, RecyclerView.ViewHolder item) {
+ if ((info.holder != null) && ((item == null) || (info.holder == item))) {
+ onAnimationEndedBeforeStarted(info, info.holder);
+ dispatchFinished(info, info.holder);
+ info.clear(info.holder);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public abstract boolean addPendingAnimation(RecyclerView.ViewHolder item);
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAnimationInfo.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAnimationInfo.java
new file mode 100644
index 00000000000..a85e1582b8d
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAnimationInfo.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.animator.impl;
+
+import android.support.v7.widget.RecyclerView;
+
+public abstract class ItemAnimationInfo {
+ public abstract RecyclerView.ViewHolder getAvailableViewHolder();
+
+ public abstract void clear(RecyclerView.ViewHolder holder);
+}
+
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemChangeAnimationManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemChangeAnimationManager.java
new file mode 100644
index 00000000000..62620ee9d97
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemChangeAnimationManager.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.animator.impl;
+
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+
+import com.h6ah4i.android.widget.advrecyclerview.animator.BaseItemAnimator;
+
+public abstract class ItemChangeAnimationManager extends BaseItemAnimationManager {
+ private static final String TAG = "ARVItemChangeAnimMgr";
+
+ public ItemChangeAnimationManager(BaseItemAnimator itemAnimator) {
+ super(itemAnimator);
+ }
+
+ @Override
+ public void dispatchStarting(ChangeAnimationInfo info, RecyclerView.ViewHolder item) {
+ if (debugLogEnabled()) {
+ Log.d(TAG, "dispatchChangeStarting(" + item + ")");
+ }
+ mItemAnimator.dispatchChangeStarting(item, (item == info.oldHolder));
+ }
+
+ @Override
+ public void dispatchFinished(ChangeAnimationInfo info, RecyclerView.ViewHolder item) {
+ if (debugLogEnabled()) {
+ Log.d(TAG, "dispatchChangeFinished(" + item + ")");
+ }
+ mItemAnimator.dispatchChangeFinished(item, (item == info.oldHolder));
+ }
+
+ @Override
+ public long getDuration() {
+ return mItemAnimator.getChangeDuration();
+ }
+
+ @Override
+ public void setDuration(long duration) {
+ mItemAnimator.setChangeDuration(duration);
+ }
+
+ @Override
+ protected void onCreateAnimation(ChangeAnimationInfo info) {
+ if (info.oldHolder != null && info.oldHolder.itemView != null) {
+ onCreateChangeAnimationForOldItem(info);
+ }
+
+ if (info.newHolder != null && info.newHolder.itemView != null) {
+ onCreateChangeAnimationForNewItem(info);
+ }
+ }
+
+ @Override
+ protected boolean endNotStartedAnimation(ChangeAnimationInfo info, RecyclerView.ViewHolder item) {
+ if ((info.oldHolder != null) && ((item == null) || (info.oldHolder == item))) {
+ onAnimationEndedBeforeStarted(info, info.oldHolder);
+ dispatchFinished(info, info.oldHolder);
+ info.clear(info.oldHolder);
+ }
+
+ if ((info.newHolder != null) && ((item == null) || (info.newHolder == item))) {
+ onAnimationEndedBeforeStarted(info, info.newHolder);
+ dispatchFinished(info, info.newHolder);
+ info.clear(info.newHolder);
+ }
+
+ return (info.oldHolder == null && info.newHolder == null);
+ }
+
+ protected abstract void onCreateChangeAnimationForNewItem(ChangeAnimationInfo info);
+
+ protected abstract void onCreateChangeAnimationForOldItem(ChangeAnimationInfo info);
+
+ public abstract boolean addPendingAnimation(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY);
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemMoveAnimationManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemMoveAnimationManager.java
new file mode 100644
index 00000000000..1ba2bd33aa5
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemMoveAnimationManager.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.animator.impl;
+
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+
+import com.h6ah4i.android.widget.advrecyclerview.animator.BaseItemAnimator;
+
+public abstract class ItemMoveAnimationManager extends BaseItemAnimationManager {
+ public static final String TAG = "ARVItemMoveAnimMgr";
+
+ public ItemMoveAnimationManager(BaseItemAnimator itemAnimator) {
+ super(itemAnimator);
+ }
+
+ @Override
+ public long getDuration() {
+ return mItemAnimator.getMoveDuration();
+ }
+
+ @Override
+ public void setDuration(long duration) {
+ mItemAnimator.setMoveDuration(duration);
+ }
+
+ @Override
+ public void dispatchStarting(MoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ if (debugLogEnabled()) {
+ Log.d(TAG, "dispatchMoveStarting(" + item + ")");
+ }
+ mItemAnimator.dispatchMoveStarting(item);
+ }
+
+ @Override
+ public void dispatchFinished(MoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ if (debugLogEnabled()) {
+ Log.d(TAG, "dispatchMoveFinished(" + item + ")");
+ }
+ mItemAnimator.dispatchMoveFinished(item);
+ }
+
+ @Override
+ protected boolean endNotStartedAnimation(MoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ if ((info.holder != null) && ((item == null) || (info.holder == item))) {
+ onAnimationEndedBeforeStarted(info, info.holder);
+ dispatchFinished(info, info.holder);
+ info.clear(info.holder);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public abstract boolean addPendingAnimation(RecyclerView.ViewHolder item, int fromX, int fromY, int toX, int toY);
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemRemoveAnimationManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemRemoveAnimationManager.java
new file mode 100644
index 00000000000..c5c3117b3f7
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemRemoveAnimationManager.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.animator.impl;
+
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+
+import com.h6ah4i.android.widget.advrecyclerview.animator.BaseItemAnimator;
+
+public abstract class ItemRemoveAnimationManager extends BaseItemAnimationManager {
+ private static final String TAG = "ARVItemRemoveAnimMgr";
+
+ public ItemRemoveAnimationManager(BaseItemAnimator itemAnimator) {
+ super(itemAnimator);
+ }
+
+ @Override
+ public long getDuration() {
+ return mItemAnimator.getRemoveDuration();
+ }
+
+ @Override
+ public void setDuration(long duration) {
+ mItemAnimator.setRemoveDuration(duration);
+ }
+
+ @Override
+ public void dispatchStarting(RemoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ if (debugLogEnabled()) {
+ Log.d(TAG, "dispatchRemoveStarting(" + item + ")");
+ }
+ mItemAnimator.dispatchRemoveStarting(item);
+ }
+
+ @Override
+ public void dispatchFinished(RemoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ if (debugLogEnabled()) {
+ Log.d(TAG, "dispatchRemoveFinished(" + item + ")");
+ }
+ mItemAnimator.dispatchRemoveFinished(item);
+ }
+
+ @Override
+ protected boolean endNotStartedAnimation(RemoveAnimationInfo info, RecyclerView.ViewHolder item) {
+ if ((info.holder != null) && ((item == null) || (info.holder == item))) {
+ onAnimationEndedBeforeStarted(info, info.holder);
+ dispatchFinished(info, info.holder);
+ info.clear(info.holder);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public abstract boolean addPendingAnimation(RecyclerView.ViewHolder holder);
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/MoveAnimationInfo.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/MoveAnimationInfo.java
new file mode 100644
index 00000000000..e30e4fc9fa5
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/MoveAnimationInfo.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.animator.impl;
+
+import android.support.v7.widget.RecyclerView;
+
+public class MoveAnimationInfo extends ItemAnimationInfo {
+ public RecyclerView.ViewHolder holder;
+ public int fromX, fromY, toX, toY;
+
+ public MoveAnimationInfo(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;
+ }
+
+ @Override
+ public RecyclerView.ViewHolder getAvailableViewHolder() {
+ return holder;
+ }
+
+ @Override
+ public void clear(RecyclerView.ViewHolder item) {
+ if (holder == item) {
+ holder = null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "MoveAnimationInfo{" +
+ "holder=" + holder +
+ ", fromX=" + fromX +
+ ", fromY=" + fromY +
+ ", toX=" + toX +
+ ", toY=" + toY +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/RemoveAnimationInfo.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/RemoveAnimationInfo.java
new file mode 100644
index 00000000000..131b6fcf976
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/RemoveAnimationInfo.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.animator.impl;
+
+import android.support.v7.widget.RecyclerView;
+
+public class RemoveAnimationInfo extends ItemAnimationInfo {
+ public RecyclerView.ViewHolder holder;
+
+ public RemoveAnimationInfo(RecyclerView.ViewHolder holder) {
+ this.holder = holder;
+ }
+
+ @Override
+ public RecyclerView.ViewHolder getAvailableViewHolder() {
+ return holder;
+ }
+
+ @Override
+ public void clear(RecyclerView.ViewHolder item) {
+ if (holder == item) {
+ holder = null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "RemoveAnimationInfo{" +
+ "holder=" + holder +
+ '}';
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/ItemShadowDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/ItemShadowDecorator.java
new file mode 100644
index 00000000000..d638bf6f3f1
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/ItemShadowDecorator.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.decoration;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.NinePatchDrawable;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+/**
+ * Item decoration which draws drop shadow of each item views.
+ */
+public class ItemShadowDecorator extends RecyclerView.ItemDecoration {
+ private final NinePatchDrawable mShadowDrawable;
+ private final Rect mShadowPadding = new Rect();
+
+ /**
+ * Constructor.
+ *
+ * @param shadow 9-patch drawable used for drop shadow
+ */
+ public ItemShadowDecorator(NinePatchDrawable shadow) {
+ mShadowDrawable = shadow;
+ mShadowDrawable.getPadding(mShadowPadding);
+ }
+
+ @Override
+ public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ final int childCount = parent.getChildCount();
+
+ if (childCount == 0) {
+ return;
+ }
+
+ int savedCount = c.save(Canvas.CLIP_SAVE_FLAG);
+
+ c.clipRect(
+ parent.getLeft() + Math.max(0, parent.getPaddingLeft() - mShadowPadding.left),
+ parent.getTop()/* + Math.max(0, parent.getPaddingTop() - mShadowPadding.top)*/,
+ parent.getRight() - Math.max(0, parent.getPaddingRight() - mShadowPadding.right),
+ parent.getBottom()/* - Math.max(0, parent.getPaddingBottom() - mShadowPadding.bottom)*/);
+
+ for (int i = 0; i < childCount; i++) {
+ final View child = parent.getChildAt(i);
+
+ if (!shouldDrawDropShadow(child)) {
+ continue;
+ }
+
+ final int tx = (int) (ViewCompat.getTranslationX(child) + 0.5f);
+ final int ty = (int) (ViewCompat.getTranslationY(child) + 0.5f);
+
+ final int left = child.getLeft() - mShadowPadding.left;
+ final int right = child.getRight() + mShadowPadding.right;
+ final int top = child.getTop() - mShadowPadding.top;
+ final int bottom = child.getBottom() + mShadowPadding.bottom;
+
+ mShadowDrawable.setBounds(left + tx, top + ty, right + tx, bottom + ty);
+ mShadowDrawable.draw(c);
+ }
+
+ c.restoreToCount(savedCount);
+
+ ViewCompat.postInvalidateOnAnimation(parent);
+ }
+
+ private static boolean shouldDrawDropShadow(View child) {
+ if (child.getVisibility() != View.VISIBLE) {
+ return false;
+ }
+ if (ViewCompat.getAlpha(child) != 1.0f) {
+ return false;
+ }
+
+ Drawable background = child.getBackground();
+ if (background == null) {
+ return false;
+ }
+
+ if (background instanceof ColorDrawable) {
+ //noinspection RedundantCast
+ if (((ColorDrawable) background).getAlpha() == 0) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ outRect.set(0, 0, 0, 0);
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/SimpleListDividerDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/SimpleListDividerDecorator.java
new file mode 100644
index 00000000000..bde3277e695
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/SimpleListDividerDecorator.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.decoration;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+/**
+ * Item decoration which draws item divider between each items.
+ */
+public class SimpleListDividerDecorator extends RecyclerView.ItemDecoration {
+ private final Drawable mDividerDrawable;
+ private final int mDividerHeight;
+ private boolean mOverlap;
+
+ /**
+ * Constructor.
+ *
+ * @param divider divider drawable
+ * @param overlap whether the divider is drawn overlapped on bottom of the item.
+ */
+ public SimpleListDividerDecorator(Drawable divider, boolean overlap) {
+ mDividerDrawable = divider;
+ mDividerHeight = mDividerDrawable.getIntrinsicHeight();
+ mOverlap = overlap;
+ }
+
+ @Override
+ public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ final int childCount = parent.getChildCount();
+ final float yPositionThreshold = (mOverlap) ? 1.0f : (mDividerHeight + 1.0f); // [px]
+ final float zPositionThreshold = 1.0f; // [px]
+
+ if (childCount == 0) {
+ return;
+ }
+
+ int savedCount = c.save(Canvas.CLIP_SAVE_FLAG);
+
+ c.clipRect(
+ parent.getLeft() + parent.getPaddingLeft(),
+ parent.getTop() + parent.getPaddingTop(),
+ parent.getRight() - parent.getPaddingRight(),
+ parent.getBottom() + parent.getPaddingBottom());
+
+ for (int i = 0; i < childCount - 1; i++) {
+ final View child = parent.getChildAt(i);
+ final View nextChild = parent.getChildAt(i + 1);
+
+ if ((child.getVisibility() != View.VISIBLE) ||
+ (nextChild.getVisibility() != View.VISIBLE)) {
+ continue;
+ }
+
+ // check if the next item is placed at the bottom
+ final float childBottom = child.getBottom() + ViewCompat.getTranslationY(child);
+ final float nextChildTop = nextChild.getTop() + ViewCompat.getTranslationY(nextChild);
+
+ if (!(Math.abs(nextChildTop - childBottom) < yPositionThreshold)) {
+ continue;
+ }
+
+ // check if the next item is placed on the same plane
+ final float childZ = ViewCompat.getTranslationZ(child) + ViewCompat.getElevation(child);
+ final float nextChildZ = ViewCompat.getTranslationZ(nextChild) + ViewCompat.getElevation(nextChild);
+
+ if (!(Math.abs(nextChildZ - childZ) < zPositionThreshold)) {
+ continue;
+ }
+
+ final float childAlpha = ViewCompat.getAlpha(child);
+ final float nextChildAlpha = ViewCompat.getAlpha(nextChild);
+
+ final int tx = (int) (ViewCompat.getTranslationX(child) + 0.5f);
+ final int ty = (int) (ViewCompat.getTranslationY(child) + 0.5f);
+ final int left = child.getLeft();
+ final int right = child.getRight();
+ final int top = child.getBottom();
+ final int bottom = top + mDividerHeight;
+
+ mDividerDrawable.setAlpha((int) ((0.5f * 255) * (childAlpha + nextChildAlpha) + 0.5f));
+ mDividerDrawable.setBounds(left + tx, top + ty, right + tx, bottom + ty);
+ mDividerDrawable.draw(c);
+ }
+
+ c.restoreToCount(savedCount);
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ if (mOverlap) {
+ outRect.set(0, 0, 0, 0);
+ } else {
+ outRect.set(0, 0, 0, mDividerHeight);
+ }
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BaseDraggableItemDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BaseDraggableItemDecorator.java
new file mode 100644
index 00000000000..4acd9ec22d7
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BaseDraggableItemDecorator.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.draggable;
+
+import android.os.Build;
+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.Interpolator;
+
+abstract class BaseDraggableItemDecorator extends RecyclerView.ItemDecoration {
+
+ private static final int RETURN_TO_DEFAULT_POS_ANIMATE_THRESHOLD_DP = 2;
+ private static final int RETURN_TO_DEFAULT_POS_ANIMATE_THRESHOLD_MSEC = 20;
+
+ private int mReturnToDefaultPositionDuration = 200;
+ private int mReturnToDefaultPositionAnimateThreshold;
+ private Interpolator mReturnToDefaultPositionInterpolator;
+
+ protected RecyclerView mRecyclerView;
+ protected RecyclerView.ViewHolder mDraggingItem;
+
+ public BaseDraggableItemDecorator(RecyclerView recyclerView, RecyclerView.ViewHolder draggingItem) {
+ mRecyclerView = recyclerView;
+ mDraggingItem = draggingItem;
+
+ final float displayDensity = recyclerView.getResources().getDisplayMetrics().density;
+ mReturnToDefaultPositionAnimateThreshold = (int) (RETURN_TO_DEFAULT_POS_ANIMATE_THRESHOLD_DP * displayDensity + 0.5f);
+ }
+
+
+ public void setReturnToDefaultPositionAnimationDuration(int duration) {
+ mReturnToDefaultPositionDuration = duration;
+ }
+
+ public void setReturnToDefaultPositionAnimationInterpolator(Interpolator interpolator) {
+ mReturnToDefaultPositionInterpolator = interpolator;
+ }
+
+ protected void moveToDefaultPosition(View targetView, boolean animate) {
+ final int curTranslationY = (int) (ViewCompat.getTranslationY(targetView));
+ final int halfItemHeight = targetView.getHeight() / 2;
+ final float translationProportion = (halfItemHeight > 0) ? Math.abs((float) curTranslationY / halfItemHeight) : 0;
+ final float t = 1.0f - Math.min(translationProportion, 1.0f);
+ final int animDuration = (int) (mReturnToDefaultPositionDuration * (1.0f - (t * t)) + 0.5f);
+
+ if (supportsViewPropertyAnimation() && animate &&
+ (animDuration > RETURN_TO_DEFAULT_POS_ANIMATE_THRESHOLD_MSEC) &&
+ (Math.abs(curTranslationY) > mReturnToDefaultPositionAnimateThreshold)) {
+ final ViewPropertyAnimatorCompat animator = ViewCompat.animate(targetView);
+ animator.cancel();
+ animator.setDuration(animDuration);
+ animator.setInterpolator(mReturnToDefaultPositionInterpolator);
+ animator.translationY(0.0f);
+ animator.setListener(new ViewPropertyAnimatorListener() {
+ @Override
+ public void onAnimationStart(View view) {
+ }
+
+ @Override
+ public void onAnimationEnd(View view) {
+ animator.setListener(null);
+ ViewCompat.setTranslationY(view, 0);
+
+ // invalidate explicitly to refresh other decorations
+ if (view.getParent() instanceof RecyclerView) {
+ ViewCompat.postInvalidateOnAnimation((RecyclerView) view.getParent());
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(View view) {
+ }
+ });
+ animator.start();
+ } else {
+ ViewCompat.setTranslationY(targetView, 0);
+ }
+ }
+
+ protected static void setItemTranslationY(RecyclerView rv, RecyclerView.ViewHolder holder, float y) {
+ final RecyclerView.ItemAnimator itemAnimator = rv.getItemAnimator();
+ if (itemAnimator != null) {
+ itemAnimator.endAnimation(holder);
+ }
+ ViewCompat.setTranslationY(holder.itemView, y);
+ }
+
+ private static boolean supportsViewPropertyAnimation() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BasicSwapTargetTranslationInterpolator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BasicSwapTargetTranslationInterpolator.java
new file mode 100644
index 00000000000..a3556622182
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BasicSwapTargetTranslationInterpolator.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.draggable;
+
+
+import android.view.animation.Interpolator;
+
+public class BasicSwapTargetTranslationInterpolator implements Interpolator {
+ private final float mThreshold;
+ private final float mHalfValidRange;
+ private final float mInvValidRange;
+
+ public BasicSwapTargetTranslationInterpolator() {
+ this(0.3f);
+ }
+
+ public BasicSwapTargetTranslationInterpolator(float threshold) {
+ if (!(threshold >= 0 && threshold < 0.5f)) {
+ throw new IllegalArgumentException("Invalid threshold range: " + threshold);
+ }
+ final float validRange = 1.0f - 2 * threshold;
+
+ mThreshold = threshold;
+ mHalfValidRange = validRange * 0.5f;
+ mInvValidRange = 1.0f / validRange;
+ }
+
+ @Override
+ public float getInterpolation(float input) {
+ if (Math.abs(input - 0.5f) < mHalfValidRange) {
+ return (input - mThreshold) * mInvValidRange;
+ } else {
+ return (input < 0.5f) ? 0.0f : 1.0f;
+ }
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemAdapter.java
new file mode 100644
index 00000000000..bf257edb9ca
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemAdapter.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.draggable;
+
+import android.support.v7.widget.RecyclerView;
+
+public interface DraggableItemAdapter {
+
+ /**
+ * Called when user is attempt to drag the item.
+ *
+ * @param holder The ViewHolder which is associated to item user is attempt to start dragging.
+ * @param x Touched X position. Relative from the itemView's top-left.
+ * @param y Touched Y position. Relative from the itemView's top-left.
+
+ * @return Whether can start dragging.
+ */
+ boolean onCheckCanStartDrag(T holder, int x, int y);
+
+ /**
+ * Called after the {@link #onCheckCanStartDrag(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method returned true.
+ *
+ * @param holder The ViewHolder which is associated to item user is attempt to start dragging.
+ *
+ * @return null: no constraints (= new ItemDraggableRange(0, getItemCount() - 1)),
+ * otherwise: the range specified item can be drag-sortable.
+ */
+ ItemDraggableRange onGetItemDraggableRange(T holder);
+
+ /**
+ * Called when item is moved. Should apply the move operation result to data set.
+ *
+ * @param fromPosition Previous position of the item.
+ * @param toPosition New position of the item.
+ */
+ void onMoveItem(int fromPosition, int toPosition);
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemViewHolder.java
new file mode 100644
index 00000000000..370cdb3be01
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemViewHolder.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.draggable;
+
+/**
+ * Interface which provides required information for dragging item.
+ *
+ * Implement this interface on your sub-class of the {@link android.support.v7.widget.RecyclerView.ViewHolder}.
+ */
+public interface DraggableItemViewHolder {
+ /**
+ * Sets the state flags value for dragging item
+ *
+ * @param flags Bitwise OR of these flags;
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_DRAGGING}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_IS_ACTIVE}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_IS_IN_RANGE}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_IS_UPDATED}
+ */
+ void setDragStateFlags(int flags);
+
+ /**
+ * Gets the state flags value for dragging item
+ *
+ * @return Bitwise OR of these flags;
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_DRAGGING}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_IS_ACTIVE}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_IS_IN_RANGE}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager#STATE_FLAG_IS_UPDATED}
+ */
+ int getDragStateFlags();
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemWrapperAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemWrapperAdapter.java
new file mode 100644
index 00000000000..f74d9515520
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemWrapperAdapter.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.draggable;
+
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.ViewGroup;
+
+import com.h6ah4i.android.widget.advrecyclerview.utils.BaseWrapperAdapter;
+import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils;
+
+class DraggableItemWrapperAdapter extends BaseWrapperAdapter {
+ private static final String TAG = "ARVDraggableWrapper";
+
+ private static final int STATE_FLAG_INITIAL_VALUE = -1;
+
+ private static final boolean LOCAL_LOGV = false;
+ private static final boolean LOCAL_LOGD = false;
+ private static final boolean DEBUG_BYPASS_MOVE_OPERATION_MODE = false;
+
+ private RecyclerViewDragDropManager mDragDropManager;
+ private DraggableItemAdapter mDraggableItemAdapter;
+ private RecyclerView.ViewHolder mDraggingItem;
+ private ItemDraggableRange mDraggableRange;
+ private long mDraggingItemId = RecyclerView.NO_ID;
+ private int mDraggingItemInitialPosition = RecyclerView.NO_POSITION;
+ private int mDraggingItemCurrentPosition = RecyclerView.NO_POSITION;
+
+ public DraggableItemWrapperAdapter(RecyclerViewDragDropManager manager, RecyclerView.Adapter adapter) {
+ super(adapter);
+
+ mDraggableItemAdapter = getDraggableItemAdapter(adapter);
+ if (getDraggableItemAdapter(adapter) == null) {
+ throw new IllegalArgumentException("adapter does not implement DraggableItemAdapter");
+ }
+
+ if (manager == null) {
+ throw new IllegalArgumentException("manager cannot be null");
+ }
+
+ mDragDropManager = manager;
+ }
+
+ @Override
+ protected void onRelease() {
+ super.onRelease();
+ mDraggingItem = null;
+ mDraggableItemAdapter = null;
+ mDragDropManager = null;
+ }
+
+ @Override
+ public VH onCreateViewHolder(ViewGroup parent, int viewType) {
+ final VH holder = super.onCreateViewHolder(parent, viewType);
+
+ if (holder instanceof DraggableItemViewHolder) {
+ ((DraggableItemViewHolder) holder).setDragStateFlags(STATE_FLAG_INITIAL_VALUE);
+ }
+
+ return holder;
+ }
+
+ @Override
+ public void onBindViewHolder(VH holder, int position) {
+ if (isDragging()) {
+ final long itemId = holder.getItemId();
+
+ final int origPosition = convertToOriginalPosition(
+ position, mDraggingItemInitialPosition, mDraggingItemCurrentPosition);
+
+ if (itemId == mDraggingItemId && holder != mDraggingItem) {
+ Log.w(TAG, "an another view holder object for the currently dragging item is assigned");
+ }
+
+ int flags = RecyclerViewDragDropManager.STATE_FLAG_DRAGGING;
+
+ if (itemId == mDraggingItemId) {
+ flags |= RecyclerViewDragDropManager.STATE_FLAG_IS_ACTIVE;
+ }
+ if (mDraggableRange.checkInRange(position)) {
+ flags |= RecyclerViewDragDropManager.STATE_FLAG_IS_IN_RANGE;
+ }
+
+ safeUpdateFlags(holder, flags);
+ super.onBindViewHolder(holder, origPosition);
+ } else {
+ safeUpdateFlags(holder, 0);
+ super.onBindViewHolder(holder, position);
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (isDragging()) {
+ final int origPosition = convertToOriginalPosition(
+ position, mDraggingItemInitialPosition, mDraggingItemCurrentPosition);
+ return super.getItemId(origPosition);
+ } else {
+ return super.getItemId(position);
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (isDragging()) {
+ final int origPosition = convertToOriginalPosition(
+ position, mDraggingItemInitialPosition, mDraggingItemCurrentPosition);
+ return super.getItemViewType(origPosition);
+ } else {
+ return super.getItemViewType(position);
+ }
+ }
+
+ protected static int convertToOriginalPosition(int position, int dragInitial, int dragCurrent) {
+ if (dragInitial < 0 || dragCurrent < 0) {
+ // not dragging
+ return position;
+ } else {
+ if ((dragInitial == dragCurrent) ||
+ ((position < dragInitial) && (position < dragCurrent)) ||
+ (position > dragInitial) && (position > dragCurrent)) {
+ return position;
+ } else if (dragCurrent < dragInitial) {
+ if (position == dragCurrent) {
+ return dragInitial;
+ } else {
+ return position - 1;
+ }
+ } else { // if (dragCurrent > dragInitial)
+ if (position == dragCurrent) {
+ return dragInitial;
+ } else {
+ return position + 1;
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onHandleWrappedAdapterChanged() {
+ if (shouldCancelDragOnDataUpdated()) {
+ cancelDrag();
+ } else {
+ super.onHandleWrappedAdapterChanged();
+ }
+ }
+
+ @Override
+ protected void onHandleWrappedAdapterItemRangeChanged(int positionStart, int itemCount) {
+ if (shouldCancelDragOnDataUpdated()) {
+ cancelDrag();
+ } else {
+ super.onHandleWrappedAdapterItemRangeChanged(positionStart, itemCount);
+ }
+ }
+
+ @Override
+ protected void onHandleWrappedAdapterItemRangeInserted(int positionStart, int itemCount) {
+ if (shouldCancelDragOnDataUpdated()) {
+ cancelDrag();
+ } else {
+ super.onHandleWrappedAdapterItemRangeInserted(positionStart, itemCount);
+ }
+ }
+
+ @Override
+ protected void onHandleWrappedAdapterItemRangeRemoved(int positionStart, int itemCount) {
+ if (shouldCancelDragOnDataUpdated()) {
+ cancelDrag();
+ } else {
+ super.onHandleWrappedAdapterItemRangeRemoved(positionStart, itemCount);
+ }
+ }
+
+ @Override
+ protected void onHandleWrappedAdapterRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ if (shouldCancelDragOnDataUpdated()) {
+ cancelDrag();
+ } else {
+ super.onHandleWrappedAdapterRangeMoved(fromPosition, toPosition, itemCount);
+ }
+ }
+
+ private boolean shouldCancelDragOnDataUpdated() {
+ //noinspection SimplifiableIfStatement
+ if (DEBUG_BYPASS_MOVE_OPERATION_MODE) {
+ return false;
+ }
+ return isDragging();
+ }
+
+ private void cancelDrag() {
+ if (mDragDropManager != null) {
+ mDragDropManager.cancelDrag();
+ }
+ }
+
+ // NOTE: This method is called from RecyclerViewDragDropManager
+ /*package*/ void onDragItemStarted(RecyclerView.ViewHolder holder, ItemDraggableRange range) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "onDragItemStarted(holder = " + holder + ")");
+ }
+
+ if (DEBUG_BYPASS_MOVE_OPERATION_MODE) {
+ return;
+ }
+
+ if (holder.getItemId() == RecyclerView.NO_ID) {
+ throw new IllegalStateException("dragging target must provides valid ID");
+ }
+
+ mDraggingItemInitialPosition = holder.getPosition();
+ mDraggingItemCurrentPosition = mDraggingItemInitialPosition;
+ mDraggingItem = holder;
+ mDraggingItemId = holder.getItemId();
+ mDraggableRange = range;
+
+ notifyDataSetChanged();
+ }
+
+ // NOTE: This method is called from RecyclerViewDragDropManager
+ /*package*/ void onDragItemFinished(RecyclerView.ViewHolder holder, boolean result) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "onDragItemFinished(holder = " + holder + ", result = " + result + ")");
+ }
+
+ if (holder != mDraggingItem) {
+ throw new IllegalStateException("onDragItemFinished() - may be a bug (mDraggingItem != holder)");
+ }
+
+ if (DEBUG_BYPASS_MOVE_OPERATION_MODE) {
+ return;
+ }
+
+ if (result && (mDraggingItemCurrentPosition != mDraggingItemInitialPosition)) {
+ // apply to wrapped adapter
+ ((DraggableItemAdapter) getWrappedAdapter()).onMoveItem(
+ mDraggingItemInitialPosition, mDraggingItemCurrentPosition);
+ }
+
+ mDraggingItemInitialPosition = RecyclerView.NO_POSITION;
+ mDraggingItemCurrentPosition = RecyclerView.NO_POSITION;
+ mDraggableRange = null;
+ mDraggingItemId = RecyclerView.NO_ID;
+ mDraggingItem = null;
+
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onViewRecycled(VH holder) {
+ if (isDragging()) {
+ if (holder == mDraggingItem) {
+ try {
+ throw new IllegalStateException("onViewRecycled() - may be a bug. dragging item should be recycled.");
+ } catch (IllegalStateException e) {
+ // TODO is there any way to prevent the dragging item from be recycled...?
+
+ mDragDropManager.cancelDrag();
+ }
+ }
+ }
+
+ super.onViewRecycled(holder);
+ }
+
+ // NOTE: This method is called from RecyclerViewDragDropManager
+ /*package*/
+ @SuppressWarnings("unchecked")
+ boolean canStartDrag(RecyclerView.ViewHolder holder, int x, int y) {
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "canStartDrag(holder = " + holder + ", x = " + x + ", y = " + y + ")");
+ }
+ return mDraggableItemAdapter.onCheckCanStartDrag(holder, x, y);
+ }
+
+ // NOTE: This method is called from RecyclerViewDragDropManager
+ /*package*/
+ @SuppressWarnings("unchecked")
+ ItemDraggableRange getItemDraggableRange(RecyclerView.ViewHolder holder) {
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "getItemDraggableRange(holder = " + holder + ")");
+ }
+ return mDraggableItemAdapter.onGetItemDraggableRange(holder);
+ }
+
+ // NOTE: This method is called from RecyclerViewDragDropManager
+ /*package*/ void moveItem(int fromPosition, int toPosition) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "onMoveItem(fromPosition = " + fromPosition + ", toPosition = " + toPosition + ")");
+ }
+
+ if (DEBUG_BYPASS_MOVE_OPERATION_MODE) {
+ mDraggableItemAdapter.onMoveItem(fromPosition, toPosition);
+ return;
+ }
+
+ final int origFromPosition = convertToOriginalPosition(
+ fromPosition, mDraggingItemInitialPosition, mDraggingItemCurrentPosition);
+
+ if (origFromPosition != mDraggingItemInitialPosition) {
+ throw new IllegalStateException(
+ "onMoveItem() - may be a bug or has duplicate IDs --- " +
+ "mDraggingItemInitialPosition = " + mDraggingItemInitialPosition + ", " +
+ "mDraggingItemCurrentPosition = " + mDraggingItemCurrentPosition + ", " +
+ "origFromPosition = " + origFromPosition + ", " +
+ "fromPosition = " + fromPosition + ", " +
+ "toPosition = " + toPosition);
+ }
+
+ mDraggingItemCurrentPosition = toPosition;
+
+ // NOTE:
+ // Don't move items in wrapped adapter here.
+
+ // notify to observers
+ notifyItemMoved(fromPosition, toPosition);
+ }
+
+ protected boolean isDragging() {
+ return mDragDropManager.isDragging();
+ }
+
+ private static void safeUpdateFlags(RecyclerView.ViewHolder holder, int flags) {
+ if (!(holder instanceof DraggableItemViewHolder)) {
+ return;
+ }
+
+ final DraggableItemViewHolder holder2 = (DraggableItemViewHolder) holder;
+
+ final int curFlags = holder2.getDragStateFlags();
+ final int mask = ~RecyclerViewDragDropManager.STATE_FLAG_IS_UPDATED;
+
+ // append UPDATED flag
+ if ((curFlags == STATE_FLAG_INITIAL_VALUE) || (((curFlags ^ flags) & mask) != 0)) {
+ flags |= RecyclerViewDragDropManager.STATE_FLAG_IS_UPDATED;
+ }
+
+ ((DraggableItemViewHolder) holder).setDragStateFlags(flags);
+ }
+
+ private static DraggableItemAdapter getDraggableItemAdapter(RecyclerView.Adapter adapter) {
+ return WrapperAdapterUtils.findWrappedAdapter(adapter, DraggableItemAdapter.class);
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggingItemDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggingItemDecorator.java
new file mode 100644
index 00000000000..00e6bb4d1cd
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggingItemDecorator.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.draggable;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.NinePatchDrawable;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.h6ah4i.android.widget.advrecyclerview.utils.CustomRecyclerViewUtils;
+
+class DraggingItemDecorator extends BaseDraggableItemDecorator {
+ @SuppressWarnings("unused")
+ private static final String TAG = "DraggingItemDecorator";
+
+ private int mGrabbedPositionY;
+ private int mTranslationY;
+ private int mRecyclerViewPaddingLeft;
+ private Bitmap mDraggingItemImage;
+ private int mTranslationTopLimit;
+ private int mTranslationBottomLimit;
+ private int mGrabbedItemHeight;
+ private int mTouchPositionY;
+ private NinePatchDrawable mShadowDrawable;
+ private Rect mShadowPadding = new Rect();
+ private Rect mDraggingItemMargins = new Rect();
+ private boolean mStarted;
+ private boolean mIsScrolling;
+ private ItemDraggableRange mRange;
+
+ public DraggingItemDecorator(RecyclerView recyclerView, RecyclerView.ViewHolder draggingItem, ItemDraggableRange range) {
+ super(recyclerView, draggingItem);
+ mRange = range;
+ CustomRecyclerViewUtils.getLayoutMargins(mDraggingItem.itemView, mDraggingItemMargins);
+ }
+
+ @Override
+ public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ // NOTE:
+ // On lollipop or later, View has Z-axis property and no needed to draw the dragging view manually.
+ // However, if the RecyclerView has any other decorations or RecyclerView is in scrolling state,
+ // need to draw it to avoid visual corruptions.
+ if (mDraggingItemImage != null) {
+ final float left = mRecyclerViewPaddingLeft + mDraggingItemMargins.left - mShadowPadding.left;
+ final float top = /*mDraggingItemMargins.top +*/ mTranslationY - mShadowPadding.top;
+ c.drawBitmap(mDraggingItemImage, left, top, null);
+ }
+ }
+
+ public void start(MotionEvent e, float grabbedPositionY) {
+ if (mStarted) {
+ return;
+ }
+
+ final View itemView = mDraggingItem.itemView;
+
+ mGrabbedPositionY = (int) (grabbedPositionY + 0.5f);
+
+ // draw the grabbed item on bitmap
+ mDraggingItemImage = createDraggingItemImage(itemView, mShadowDrawable);
+
+ mGrabbedItemHeight = itemView.getHeight();
+ mTranslationTopLimit = mRecyclerView.getPaddingTop();
+ mRecyclerViewPaddingLeft = mRecyclerView.getPaddingLeft();
+
+ // hide
+ itemView.setVisibility(View.INVISIBLE);
+
+ update(e);
+
+ mRecyclerView.addItemDecoration(this);
+
+ mStarted = true;
+ }
+
+ public void finish(boolean animate) {
+ if (mStarted) {
+ mRecyclerView.removeItemDecoration(this);
+ }
+
+ final RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator();
+ if (itemAnimator != null) {
+ itemAnimator.endAnimations();
+ }
+ mRecyclerView.stopScroll();
+
+ // return to default position
+ updateDraggingItemPosition(mTranslationY);
+ moveToDefaultPosition(mDraggingItem.itemView, animate);
+
+ // show
+ mDraggingItem.itemView.setVisibility(View.VISIBLE);
+ mDraggingItem = null;
+
+ if (mDraggingItemImage != null) {
+ mDraggingItemImage.recycle();
+ mDraggingItemImage = null;
+ }
+
+ mRange = null;
+ mGrabbedPositionY = 0;
+ mTranslationY = 0;
+ mTranslationTopLimit = 0;
+ mTranslationBottomLimit = 0;
+ mRecyclerViewPaddingLeft = 0;
+ mGrabbedItemHeight = 0;
+ mTouchPositionY = 0;
+ mStarted = false;
+ }
+
+ public void update(MotionEvent e) {
+ mTouchPositionY = (int) (e.getY() + 0.5f);
+ refresh();
+ }
+
+ public void refresh() {
+ updateTranslationOffset();
+ updateDraggingItemPosition(mTranslationY);
+
+ ViewCompat.postInvalidateOnAnimation(mRecyclerView);
+ }
+
+ public void setShadowDrawable(NinePatchDrawable shadowDrawable) {
+ mShadowDrawable = shadowDrawable;
+
+ if (mShadowDrawable != null) {
+ mShadowDrawable.getPadding(mShadowPadding);
+ }
+ }
+
+ public int getDraggingItemTranslationY() {
+ return mTranslationY;
+ }
+
+ private void updateTranslationOffset() {
+ final int childCount = mRecyclerView.getChildCount();
+ if (childCount > 0) {
+ mTranslationTopLimit = mRecyclerView.getPaddingTop();
+ mTranslationBottomLimit = Math.max(0, (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom() - mGrabbedItemHeight));
+
+ if (!mIsScrolling) {
+ final int firstVisiblePosition = CustomRecyclerViewUtils.findFirstVisibleItemPosition(mRecyclerView);
+ final int lastVisiblePosition = CustomRecyclerViewUtils.findLastVisibleItemPosition(mRecyclerView);
+ final View topChild = findRangeFirstItem(mRecyclerView, mRange, firstVisiblePosition, lastVisiblePosition);
+ final View bottomChild = findRangeLastItem(mRecyclerView, mRange, firstVisiblePosition, lastVisiblePosition);
+
+ if (topChild != null) {
+ mTranslationTopLimit = Math.min(mTranslationBottomLimit, topChild.getTop());
+ }
+
+ if (bottomChild != null) {
+ mTranslationBottomLimit = Math.min(mTranslationBottomLimit, bottomChild.getTop());
+ }
+ }
+ } else {
+ mTranslationBottomLimit = mTranslationTopLimit = mRecyclerView.getPaddingTop();
+ }
+
+ mTranslationY = mTouchPositionY - mGrabbedPositionY;
+ mTranslationY = Math.min(Math.max(mTranslationY, mTranslationTopLimit), mTranslationBottomLimit);
+ }
+
+
+ private Bitmap createDraggingItemImage(View v, NinePatchDrawable shadow) {
+ int width = v.getWidth() + mShadowPadding.left + mShadowPadding.right;
+ int height = v.getHeight() + mShadowPadding.top + mShadowPadding.bottom;
+
+ final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+
+ final Canvas canvas = new Canvas(bitmap);
+
+ if (shadow != null) {
+ shadow.setBounds(0, 0, width, height);
+ shadow.draw(canvas);
+ }
+
+ final int savedCount = canvas.save(Canvas.CLIP_SAVE_FLAG | Canvas.MATRIX_SAVE_FLAG);
+ // NOTE: Explicitly set clipping rect. This is required on Gingerbread.
+ canvas.clipRect(mShadowPadding.left, mShadowPadding.top, width - mShadowPadding.right, height - mShadowPadding.bottom);
+ canvas.translate(mShadowPadding.left, mShadowPadding.top);
+ v.draw(canvas);
+ canvas.restoreToCount(savedCount);
+
+ return bitmap;
+ }
+
+ private void updateDraggingItemPosition(int translationY) {
+ // NOTE: Need to update the view position to make other decorations work properly while dragging
+ setItemTranslationY(mRecyclerView, mDraggingItem, translationY - mDraggingItem.itemView.getTop());
+ }
+
+ public void setIsScrolling(boolean isScrolling) {
+ if (mIsScrolling == isScrolling) {
+ return;
+ }
+
+ mIsScrolling = isScrolling;
+
+ }
+
+ public int getTranslatedItemPositionTop() {
+ return mTranslationY;
+ }
+
+ public int getTranslatedItemPositionBottom() {
+ return mTranslationY + mGrabbedItemHeight;
+ }
+
+
+ private static View findRangeFirstItem(RecyclerView rv, ItemDraggableRange range, int firstVisiblePosition, int lastVisiblePosition) {
+ if (firstVisiblePosition == RecyclerView.NO_POSITION || lastVisiblePosition == RecyclerView.NO_POSITION) {
+ return null;
+ }
+
+ View v = null;
+
+ final int childCount = rv.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View v2 = rv.getChildAt(i);
+ final RecyclerView.ViewHolder vh = rv.getChildViewHolder(v2);
+
+ if (vh != null) {
+ final int position = vh.getPosition();
+
+ if ((position >= firstVisiblePosition) &&
+ (position <= lastVisiblePosition) &&
+ range.checkInRange(position)) {
+ v = v2;
+ break;
+ }
+
+ }
+ }
+
+ return v;
+ }
+
+ private static View findRangeLastItem(RecyclerView rv, ItemDraggableRange range, int firstVisiblePosition, int lastVisiblePosition) {
+ if (firstVisiblePosition == RecyclerView.NO_POSITION || lastVisiblePosition == RecyclerView.NO_POSITION) {
+ return null;
+ }
+
+ View v = null;
+
+ final int childCount = rv.getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View v2 = rv.getChildAt(i);
+ final RecyclerView.ViewHolder vh = rv.getChildViewHolder(v2);
+
+ if (vh != null) {
+ final int position = vh.getPosition();
+
+ if ((position >= firstVisiblePosition) &&
+ (position <= lastVisiblePosition) &&
+ range.checkInRange(position)) {
+ v = v2;
+ break;
+ }
+ }
+ }
+
+ return v;
+ }
+}
\ No newline at end of file
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/EdgeEffectDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/EdgeEffectDecorator.java
new file mode 100644
index 00000000000..372a595aec8
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/EdgeEffectDecorator.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.draggable;
+
+import android.graphics.Canvas;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.EdgeEffectCompat;
+import android.support.v7.widget.RecyclerView;
+
+public class EdgeEffectDecorator extends RecyclerView.ItemDecoration {
+ private RecyclerView mRecyclerView;
+ private EdgeEffectCompat mTopGlow;
+ private EdgeEffectCompat mBottomGlow;
+ private boolean mStarted;
+
+ public EdgeEffectDecorator(RecyclerView recyclerView) {
+ mRecyclerView = recyclerView;
+ }
+
+ @Override
+ public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ boolean needsInvalidate = false;
+
+ if (mTopGlow != null && !mTopGlow.isFinished()) {
+ final int restore = c.save();
+ if (getClipToPadding(parent)) {
+ c.translate(parent.getPaddingLeft(), parent.getPaddingTop());
+ }
+ //noinspection ConstantConditions
+ needsInvalidate |= mTopGlow.draw(c);
+ c.restoreToCount(restore);
+ }
+
+ if (mBottomGlow != null && !mBottomGlow.isFinished()) {
+ final int restore = c.save();
+ c.rotate(180);
+ if (getClipToPadding(parent)) {
+ c.translate(-parent.getWidth() + parent.getPaddingRight(), -parent.getHeight() + parent.getPaddingBottom());
+ } else {
+ c.translate(-parent.getWidth(), -parent.getHeight());
+ }
+ needsInvalidate |= mBottomGlow.draw(c);
+ c.restoreToCount(restore);
+ }
+
+ if (needsInvalidate) {
+ ViewCompat.postInvalidateOnAnimation(parent);
+ }
+ }
+
+ public void start() {
+ if (mStarted) {
+ return;
+ }
+ mRecyclerView.addItemDecoration(this);
+ mStarted = true;
+ }
+
+ public void finish() {
+ if (mStarted) {
+ mRecyclerView.removeItemDecoration(this);
+ }
+ releaseBothGlows();
+ mRecyclerView = null;
+ mStarted = false;
+ }
+
+ public void pullTopGlow(float deltaDistance) {
+ ensureTopGlow(mRecyclerView);
+
+ if (mTopGlow.onPull(deltaDistance)) {
+ ViewCompat.postInvalidateOnAnimation(mRecyclerView);
+ }
+ }
+
+ public void pullBottom(float deltaDistance) {
+ ensureBottomGlow(mRecyclerView);
+ mBottomGlow.onPull(deltaDistance);
+
+ if (mBottomGlow.onPull(deltaDistance)) {
+ ViewCompat.postInvalidateOnAnimation(mRecyclerView);
+ }
+ }
+
+ public void releaseBothGlows() {
+ boolean needsInvalidate = false;
+
+ if (mTopGlow != null) {
+ //noinspection ConstantConditions
+ needsInvalidate |= mTopGlow.onRelease();
+ }
+
+ if (mBottomGlow != null) {
+ needsInvalidate |= mBottomGlow.onRelease();
+ }
+
+ if (needsInvalidate) {
+ ViewCompat.postInvalidateOnAnimation(mRecyclerView);
+ }
+ }
+
+ private void ensureTopGlow(RecyclerView rv) {
+ if (mTopGlow != null) {
+ return;
+ }
+ mTopGlow = new EdgeEffectCompat(rv.getContext());
+ if (getClipToPadding(rv)) {
+ mTopGlow.setSize(
+ rv.getMeasuredWidth() - rv.getPaddingLeft() - rv.getPaddingRight(),
+ rv.getMeasuredHeight() - rv.getPaddingTop() - rv.getPaddingBottom());
+ } else {
+ mTopGlow.setSize(rv.getMeasuredWidth(), rv.getMeasuredHeight());
+ }
+ }
+
+ private void ensureBottomGlow(RecyclerView rv) {
+ if (mBottomGlow != null) {
+ return;
+ }
+ mBottomGlow = new EdgeEffectCompat(rv.getContext());
+ if (getClipToPadding(rv)) {
+ mBottomGlow.setSize(
+ rv.getMeasuredWidth() - rv.getPaddingLeft() - rv.getPaddingRight(),
+ rv.getMeasuredHeight() - rv.getPaddingTop() - rv.getPaddingBottom());
+ } else {
+ mBottomGlow.setSize(rv.getMeasuredWidth(), rv.getMeasuredHeight());
+ }
+ }
+
+ private static boolean getClipToPadding(RecyclerView rv) {
+ return rv.getLayoutManager().getClipToPadding();
+ }
+
+ public void reorderToTop() {
+ if (mStarted) {
+ mRecyclerView.removeItemDecoration(this);
+ mRecyclerView.addItemDecoration(this);
+ }
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/ItemDraggableRange.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/ItemDraggableRange.java
new file mode 100644
index 00000000000..2625cf5120d
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/ItemDraggableRange.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.draggable;
+
+public class ItemDraggableRange {
+ private final int mStart;
+ private final int mEnd;
+
+ public ItemDraggableRange(int start, int end) {
+ if (!(start <= end)) {
+ throw new IllegalArgumentException("end position (= " + end + ") is smaller than start position (=" + start + ")");
+ }
+
+ mStart = start;
+ mEnd = end;
+ }
+
+ public int getStart() {
+ return mStart;
+ }
+
+ public int getEnd() {
+ return mEnd;
+ }
+
+ public boolean checkInRange(int position) {
+ return ((position >= mStart) && (position <= mEnd));
+ }
+
+ @Override
+ public String toString() {
+ return "ItemDraggableRange{" +
+ "mStart=" + mStart +
+ ", mEnd=" + mEnd +
+ '}';
+ }
+}
+
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/RecyclerViewDragDropManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/RecyclerViewDragDropManager.java
new file mode 100644
index 00000000000..41667008824
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/RecyclerViewDragDropManager.java
@@ -0,0 +1,1110 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.draggable;
+
+import android.graphics.Rect;
+import android.graphics.drawable.NinePatchDrawable;
+import android.os.Build;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.h6ah4i.android.widget.advrecyclerview.event.RecyclerViewOnScrollEventDistributor;
+import com.h6ah4i.android.widget.advrecyclerview.utils.CustomRecyclerViewUtils;
+import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Provides item drag & drop operation for {@link android.support.v7.widget.RecyclerView}
+ */
+@SuppressWarnings("PointlessBitwiseExpression")
+public class RecyclerViewDragDropManager {
+ private static final String TAG = "ARVDragDropManager";
+
+ /**
+ * State flag for the {@link DraggableItemViewHolder#setDragStateFlags(int)} and {@link DraggableItemViewHolder#getDragStateFlags()} methods.
+ * Indicates that currently performing dragging.
+ */
+ public static final int STATE_FLAG_DRAGGING = (1 << 0);
+
+ /**
+ * State flag for the {@link DraggableItemViewHolder#setDragStateFlags(int)} and {@link DraggableItemViewHolder#getDragStateFlags()} methods.
+ * Indicates that this item is being dragged.
+ */
+ public static final int STATE_FLAG_IS_ACTIVE = (1 << 1);
+
+ /**
+ * State flag for the {@link DraggableItemViewHolder#setDragStateFlags(int)} and {@link DraggableItemViewHolder#getDragStateFlags()} methods.
+ * Indicates that this item is in the range of drag-sortable items
+ */
+ public static final int STATE_FLAG_IS_IN_RANGE = (1 << 2);
+
+ /**
+ * State flag for the {@link DraggableItemViewHolder#setDragStateFlags(int)} and {@link DraggableItemViewHolder#getDragStateFlags()} methods.
+ * If this flag is set, some other flags are changed and require to apply.
+ */
+ public static final int STATE_FLAG_IS_UPDATED = (1 << 31);
+
+ // ---
+
+ /**
+ * Default interpolator used for "swap target transition"
+ */
+ public static final Interpolator DEFAULT_SWAP_TARGET_TRANSITION_INTERPOLATOR = new BasicSwapTargetTranslationInterpolator();
+
+
+ /**
+ * Default interpolator used for "item settle back into place" animation
+ */
+ public static final Interpolator DEFAULT_ITEM_SETTLE_BACK_INTO_PLACE_ANIMATION_INTERPOLATOR = new DecelerateInterpolator();
+
+ // ---
+
+ private static final int SCROLL_DIR_NONE = 0;
+ private static final int SCROLL_DIR_UP = (1 << 0);
+ private static final int SCROLL_DIR_DOWN = (1 << 1);
+
+ private static final boolean LOCAL_LOGV = false;
+ private static final boolean LOCAL_LOGD = false;
+
+ private static final float SCROLL_THRESHOLD = 0.3f; // 0.0f < X < 0.5f
+ private static final float SCROLL_AMOUNT_COEFF = 25;
+ private static final float EDGE_EFFECT_THRESHOLD = 0.5f; // <= 1.0f
+ private static final float SCROLL_TOUCH_SLOP_MULTIPLY = 1.5f;
+
+ private RecyclerView mRecyclerView;
+ private Interpolator mSwapTargetTranslationInterpolator = DEFAULT_SWAP_TARGET_TRANSITION_INTERPOLATOR;
+ private ScrollOnDraggingProcessRunnable mScrollOnDraggingProcess;
+ private boolean mScrollEventRegisteredToDistributor;
+
+ private RecyclerView.OnItemTouchListener mInternalUseOnItemTouchListener;
+ private RecyclerView.OnScrollListener mInternalUseOnScrollListener;
+
+ private EdgeEffectDecorator mEdgeEffectDecorator;
+ private NinePatchDrawable mShadowDrawable;
+
+ private float mDisplayDensity;
+ private int mTouchSlop;
+ private int mScrollTouchSlop;
+ private int mInitialTouchY;
+ private long mInitialTouchItemId = RecyclerView.NO_ID;
+
+ private boolean mInScrollByMethod;
+ private int mActualScrollByAmount;
+
+ private Rect mTmpRect1 = new Rect();
+
+ private Runnable mDeferredCancelProcess;
+ private int mItemSettleBackIntoPlaceAnimationDuration = 200;
+ private Interpolator mItemSettleBackIntoPlaceAnimationInterpolator = DEFAULT_ITEM_SETTLE_BACK_INTO_PLACE_ANIMATION_INTERPOLATOR;
+
+ // these fields are only valid while dragging
+ private DraggableItemWrapperAdapter mAdapter;
+ private long mDraggingItemId = RecyclerView.NO_ID;
+ private RecyclerView.ViewHolder mDraggingItem;
+ private Rect mDraggingItemMargins = new Rect();
+ private DraggingItemDecorator mDraggingItemDecorator;
+ private SwapTargetItemOperator mSwapTargetItemOperator;
+ private int mLastTouchY;
+ private int mDragStartTouchY;
+ private int mDragMinTouchY;
+ private int mDragMaxTouchY;
+ private int mScrollDirMask = SCROLL_DIR_NONE;
+ private int mGrabbedPositionY;
+ private int mGrabbedItemHeight;
+ private int mOrigOverScrollMode;
+ private ItemDraggableRange mDraggableRange;
+
+ /**
+ * Constructor.
+ */
+ public RecyclerViewDragDropManager() {
+ mInternalUseOnItemTouchListener = new RecyclerView.OnItemTouchListener() {
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ return RecyclerViewDragDropManager.this.onInterceptTouchEvent(rv, e);
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ RecyclerViewDragDropManager.this.onTouchEvent(rv, e);
+ }
+ };
+
+ mInternalUseOnScrollListener = new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ RecyclerViewDragDropManager.this.onScrollStateChanged(recyclerView, newState);
+ }
+
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ RecyclerViewDragDropManager.this.onScrolled(recyclerView, dx, dy);
+ }
+ };
+
+ mScrollOnDraggingProcess = new ScrollOnDraggingProcessRunnable(this);
+ }
+
+ /**
+ * Create wrapped adapter.
+ *
+ * @param adapter The target adapter.
+ *
+ * @return Wrapped adapter which is associated to this {@link RecyclerViewDragDropManager} instance.
+ */
+ @SuppressWarnings("unchecked")
+ public RecyclerView.Adapter createWrappedAdapter(RecyclerView.Adapter adapter) {
+ if (mAdapter != null) {
+ throw new IllegalStateException("already have a wrapped adapter");
+ }
+
+ mAdapter = new DraggableItemWrapperAdapter(this, adapter);
+
+ return mAdapter;
+ }
+
+ /**
+ * Indicates this manager instance has released or not.
+ *
+ * @return True if this manager instance has released
+ */
+ public boolean isReleased() {
+ return (mInternalUseOnItemTouchListener == null);
+ }
+
+ /**
+ * Attaches {@link android.support.v7.widget.RecyclerView} instance.
+ *
+ * Before calling this method, the target {@link android.support.v7.widget.RecyclerView} must set
+ * the wrapped adapter instance which is returned by the
+ * {@link #createWrappedAdapter(android.support.v7.widget.RecyclerView.Adapter)} method.
+ *
+ * @param rv The {@link android.support.v7.widget.RecyclerView} instance
+ */
+ public void attachRecyclerView(RecyclerView rv) {
+ attachRecyclerView(rv, null);
+ }
+
+ /**
+ * Attaches {@link android.support.v7.widget.RecyclerView} instance.
+ *
+ * Before calling this method, the target {@link android.support.v7.widget.RecyclerView} must set
+ * the wrapped adapter instance which is returned by the
+ * {@link #createWrappedAdapter(android.support.v7.widget.RecyclerView.Adapter)} method.
+ *
+ * @param rv The {@link android.support.v7.widget.RecyclerView} instance
+ * @param scrollEventDistributor The distributor for {@link android.support.v7.widget.RecyclerView.OnScrollListener} event
+ */
+ public void attachRecyclerView(RecyclerView rv, RecyclerViewOnScrollEventDistributor scrollEventDistributor) {
+ if (rv == null) {
+ throw new IllegalArgumentException("RecyclerView cannot be null");
+ }
+
+ if (isReleased()) {
+ throw new IllegalStateException("Accessing released object");
+ }
+
+ if (mRecyclerView != null) {
+ throw new IllegalStateException("RecyclerView instance has already been set");
+ }
+
+ if (mAdapter == null || getDraggableItemWrapperAdapter(rv) != mAdapter) {
+ throw new IllegalStateException("adapter is not set properly");
+ }
+
+ if (scrollEventDistributor != null) {
+ final RecyclerView rv2 = scrollEventDistributor.getRecyclerView();
+
+ if (rv2 != null && rv2 != rv) {
+ throw new IllegalArgumentException("The scroll event distributor attached to different RecyclerView instance");
+ }
+ }
+
+ mRecyclerView = rv;
+
+ if (scrollEventDistributor != null) {
+ scrollEventDistributor.add(mInternalUseOnScrollListener);
+ mScrollEventRegisteredToDistributor = true;
+ } else {
+ mRecyclerView.setOnScrollListener(mInternalUseOnScrollListener);
+ mScrollEventRegisteredToDistributor = false;
+ }
+
+ mRecyclerView.addOnItemTouchListener(mInternalUseOnItemTouchListener);
+
+ mDisplayDensity = mRecyclerView.getResources().getDisplayMetrics().density;
+ mTouchSlop = ViewConfiguration.get(mRecyclerView.getContext()).getScaledTouchSlop();
+ mScrollTouchSlop = (int) (mTouchSlop * SCROLL_TOUCH_SLOP_MULTIPLY + 0.5f);
+
+ if (supportsEdgeEffect()) {
+ // edge effect is available on ICS or later
+ mEdgeEffectDecorator = new EdgeEffectDecorator(mRecyclerView);
+ mEdgeEffectDecorator.start();
+ }
+ }
+
+ /**
+ * Detach the {@link android.support.v7.widget.RecyclerView} instance and release internal field references.
+ *
+ * This method should be called in order to avoid memory leaks.
+ */
+ public void release() {
+ cancelDrag();
+
+ if (mEdgeEffectDecorator != null) {
+ mEdgeEffectDecorator.finish();
+ mEdgeEffectDecorator = null;
+ }
+
+ if (mRecyclerView != null && mInternalUseOnItemTouchListener != null) {
+ mRecyclerView.removeOnItemTouchListener(mInternalUseOnItemTouchListener);
+ }
+ mInternalUseOnItemTouchListener = null;
+
+ if (mRecyclerView != null && mInternalUseOnScrollListener != null && mScrollEventRegisteredToDistributor) {
+ mRecyclerView.setOnScrollListener(null);
+ }
+ mInternalUseOnScrollListener = null;
+
+ if (mScrollOnDraggingProcess != null) {
+ mScrollOnDraggingProcess.release();
+ mScrollOnDraggingProcess = null;
+ }
+ mAdapter = null;
+ mRecyclerView = null;
+ mSwapTargetTranslationInterpolator = null;
+ mScrollEventRegisteredToDistributor = false;
+ }
+
+ /**
+ * Indicates whether currently performing item dragging.
+ *
+ * @return True if currently performing item dragging
+ */
+ public boolean isDragging() {
+ return (mDraggingItem != null) && (mDeferredCancelProcess == null);
+ }
+
+ /**
+ * Sets 9-patch image which is used for the actively dragging item
+ *
+ * @param drawable The 9-patch drawable
+ */
+ public void setDraggingItemShadowDrawable(NinePatchDrawable drawable) {
+ mShadowDrawable = drawable;
+ }
+
+ /**
+ * Sets the interpolator which is used for determining the position of the swapping item.
+ *
+ * @param interpolator Interpolator to set or null to clear
+ */
+ public void setSwapTargetTranslationInterpolator(Interpolator interpolator) {
+ mSwapTargetTranslationInterpolator = interpolator;
+ }
+
+ /**
+ * Gets the interpolator which ise used for determining the position of the swapping item.
+ *
+ * @return Interpolator which is used for determining the position of the swapping item
+ */
+ public Interpolator setSwapTargetTranslationInterpolator() {
+ return mSwapTargetTranslationInterpolator;
+ }
+
+ /*package*/ boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ final int action = MotionEventCompat.getActionMasked(e);
+
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "onInterceptTouchEvent() action = " + action);
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ handleActionUpOrCancel(rv, e);
+ break;
+
+ case MotionEvent.ACTION_DOWN:
+ if (!isDragging()) {
+ handleActionDown(rv, e);
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (isDragging()) {
+ // NOTE: The first ACTION_MOVE event will come here. (maybe a bug of RecyclerView?)
+ handleActionMoveWhileDragging(rv, e);
+ return true;
+ } else {
+ if (handleActionMoveWhileNotDragging(rv, e)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /*package*/ void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ final int action = MotionEventCompat.getActionMasked(e);
+
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "onTouchEvent() action = " + action);
+ }
+
+ if (!isDragging()) {
+ // Log.w(TAG, "onTouchEvent() - unexpected state");
+ return;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ handleActionUpOrCancel(rv, e);
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ handleActionMoveWhileDragging(rv, e);
+ break;
+
+ }
+ }
+
+ /*package*/ void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "onScrolled(dx = " + dx + ", dy = " + dy + ")");
+ }
+
+ if (mInScrollByMethod) {
+ mActualScrollByAmount = dy;
+ }
+ }
+
+ /*package*/ void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "onScrollStateChanged(newState = " + newState + ")");
+ }
+ }
+
+ private boolean handleActionDown(RecyclerView rv, MotionEvent e) {
+ final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithoutTranslation(rv, e.getX(), e.getY());
+
+ if (!checkTouchedItemState(rv, holder)) {
+ return false;
+ }
+
+ mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
+ mInitialTouchItemId = holder.getItemId();
+
+ return true;
+ }
+
+ @SuppressWarnings("unchecked")
+ private void startDragging(RecyclerView rv, MotionEvent e, RecyclerView.ViewHolder holder, ItemDraggableRange range) {
+ safeEndAnimation(rv, holder);
+
+ mDraggingItem = holder;
+
+ // XXX if setIsRecyclable() is used, another view holder objects will be created
+ // which has the same ID with currently dragging item... Not works as expected.
+
+ // mDraggingItem.setIsRecyclable(false);
+
+ mDraggingItemId = mDraggingItem.getItemId();
+ mDraggableRange = range;
+
+ final View itemView = mDraggingItem.itemView;
+
+ mOrigOverScrollMode = ViewCompat.getOverScrollMode(rv);
+ ViewCompat.setOverScrollMode(rv, ViewCompat.OVER_SCROLL_NEVER);
+
+ mLastTouchY = (int) (e.getY() + 0.5f);
+
+ // disable auto scrolling until user moves the item
+ mDragStartTouchY = mDragMinTouchY = mDragMaxTouchY = mLastTouchY;
+ mScrollDirMask = SCROLL_DIR_NONE;
+
+ // calculate the view-local offset from the touched point
+ mGrabbedPositionY = mLastTouchY - itemView.getTop();
+
+ mGrabbedItemHeight = itemView.getHeight();
+ CustomRecyclerViewUtils.getLayoutMargins(itemView, mDraggingItemMargins);
+
+ mRecyclerView.getParent().requestDisallowInterceptTouchEvent(true);
+
+ startScrollOnDraggingProcess();
+
+ // raise onDragItemStarted() event
+ mAdapter.onDragItemStarted(mDraggingItem, mDraggableRange);
+
+ // setup decorators
+ mAdapter.onBindViewHolder(mDraggingItem, mDraggingItem.getPosition());
+
+ mDraggingItemDecorator = new DraggingItemDecorator(mRecyclerView, mDraggingItem, mDraggableRange);
+ mDraggingItemDecorator.setShadowDrawable(mShadowDrawable);
+ mDraggingItemDecorator.start(e, mGrabbedPositionY);
+
+ if (supportsViewTranslation()) {
+ mSwapTargetItemOperator = new SwapTargetItemOperator(mRecyclerView, mDraggingItem, mDraggableRange);
+ mSwapTargetItemOperator.setSwapTargetTranslationInterpolator(mSwapTargetTranslationInterpolator);
+ mSwapTargetItemOperator.start();
+ mSwapTargetItemOperator.update(mDraggingItemDecorator.getDraggingItemTranslationY());
+ }
+
+ if (mEdgeEffectDecorator != null) {
+ mEdgeEffectDecorator.reorderToTop();
+ }
+ }
+
+ /**
+ * Cancel dragging.
+ */
+ public void cancelDrag() {
+ cancelDrag(false);
+ }
+
+ private void cancelDrag(boolean immediately) {
+ if (immediately) {
+ finishDragging(false);
+ } else {
+ if (mDraggingItem != null) {
+ if (mDeferredCancelProcess == null) {
+ mDeferredCancelProcess = new Runnable() {
+ @Override
+ public void run() {
+ if (mDeferredCancelProcess == this) {
+ mDeferredCancelProcess = null;
+ finishDragging(false);
+ }
+ }
+ };
+ mRecyclerView.post(mDeferredCancelProcess);
+ }
+ }
+ }
+ }
+
+ private void finishDragging(boolean result) {
+ final RecyclerView.ViewHolder draggedItem = mDraggingItem;
+
+ // cancel deferred request
+ if (mDeferredCancelProcess != null) {
+ mRecyclerView.removeCallbacks(mDeferredCancelProcess);
+ mDeferredCancelProcess = null;
+ }
+
+ // NOTE: setOverScrollMode() have to be called before calling removeItemDecoration()
+ if (mRecyclerView != null && mDraggingItem != null) {
+ ViewCompat.setOverScrollMode(mRecyclerView, mOrigOverScrollMode);
+ }
+
+ if (mDraggingItemDecorator != null) {
+ mDraggingItemDecorator.setReturnToDefaultPositionAnimationDuration(mItemSettleBackIntoPlaceAnimationDuration);
+ mDraggingItemDecorator.setReturnToDefaultPositionAnimationInterpolator(mItemSettleBackIntoPlaceAnimationInterpolator);
+ mDraggingItemDecorator.finish(true);
+ }
+
+ if (mSwapTargetItemOperator != null) {
+ mSwapTargetItemOperator.setReturnToDefaultPositionAnimationDuration(mItemSettleBackIntoPlaceAnimationDuration);
+ mDraggingItemDecorator.setReturnToDefaultPositionAnimationInterpolator(mItemSettleBackIntoPlaceAnimationInterpolator);
+ mSwapTargetItemOperator.finish(true);
+ }
+
+ if (mEdgeEffectDecorator != null) {
+ mEdgeEffectDecorator.releaseBothGlows();
+ }
+
+ stopScrollOnDraggingProcess();
+
+ if (mRecyclerView != null && mRecyclerView.getParent() != null) {
+ mRecyclerView.getParent().requestDisallowInterceptTouchEvent(false);
+ }
+
+ mDraggableRange = null;
+ mDraggingItemDecorator = null;
+ mSwapTargetItemOperator = null;
+ mDraggingItem = null;
+ mDraggingItemId = RecyclerView.NO_ID;
+
+ mLastTouchY = 0;
+ mDragStartTouchY = 0;
+ mDragMinTouchY = 0;
+ mDragMaxTouchY = 0;
+ mGrabbedPositionY = 0;
+ mGrabbedItemHeight = 0;
+
+ // raise onDragItemFinished() event
+ if ((mAdapter != null) && (draggedItem != null)) {
+ mAdapter.onDragItemFinished(draggedItem, result);
+ }
+
+// if (draggedItem != null) {
+// draggedItem.setIsRecyclable(true);
+// }
+ }
+
+ private boolean handleActionUpOrCancel(RecyclerView rv, MotionEvent e) {
+ final boolean result = (MotionEventCompat.getActionMasked(e) == MotionEvent.ACTION_UP);
+
+ mInitialTouchY = 0;
+ mLastTouchY = 0;
+ mDragStartTouchY = 0;
+ mDragMinTouchY = 0;
+ mDragMaxTouchY = 0;
+ mInitialTouchItemId = RecyclerView.NO_ID;
+
+ if (isDragging()) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "dragging finished --- result = " + result);
+ }
+
+ finishDragging(result);
+ }
+
+ return true;
+ }
+
+ private boolean handleActionMoveWhileNotDragging(RecyclerView rv, MotionEvent e) {
+ final int touchX = (int) (e.getX() + 0.5f);
+ final int touchY = (int) (e.getY() + 0.5f);
+
+ mLastTouchY = touchY;
+
+ if (mInitialTouchItemId == RecyclerView.NO_ID) {
+ return false;
+ }
+
+ if (!(Math.abs(touchY - mInitialTouchY) > mTouchSlop)) {
+ return false;
+ }
+
+ final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithoutTranslation(rv, e.getX(), e.getY());
+
+ if (!checkTouchedItemState(rv, holder)) {
+ mInitialTouchItemId = RecyclerView.NO_ID;
+ return false;
+ }
+
+ if (holder.getItemId() != mInitialTouchItemId) {
+ mInitialTouchItemId = RecyclerView.NO_ID;
+ return false;
+ }
+
+ final View view = holder.itemView;
+ final int translateX = (int) (ViewCompat.getTranslationX(view) + 0.5f);
+ final int translateY = (int) (ViewCompat.getTranslationY(view) + 0.5f);
+ final int viewX = touchX - (view.getLeft() + translateX);
+ final int viewY = touchY - (view.getTop() + translateY);
+
+ if (!mAdapter.canStartDrag(holder, viewX, viewY)) {
+ return false;
+ }
+
+ ItemDraggableRange range = mAdapter.getItemDraggableRange(holder);
+
+ if (range == null) {
+ range = new ItemDraggableRange(0, Math.max(0, mAdapter.getItemCount() - 1));
+ }
+
+ verifyItemDraggableRange(range, holder);
+
+
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "dragging started");
+ }
+
+ startDragging(rv, e, holder, range);
+
+ return true;
+
+ }
+
+ private void verifyItemDraggableRange(ItemDraggableRange range, RecyclerView.ViewHolder holder) {
+ final int start = 0;
+ final int end = Math.max(0, mAdapter.getItemCount() - 1);
+
+ if (range.getStart() > range.getEnd()) {
+ throw new IllegalStateException("Invalid range specified --- start > range (range = " + range + ")");
+ }
+
+ if (range.getStart() < start) {
+ throw new IllegalStateException("Invalid range specified --- start < 0 (range = " + range + ")");
+ }
+
+ if (range.getEnd() > end) {
+ throw new IllegalStateException("Invalid range specified --- end >= count (range = " + range + ")");
+ }
+
+ if (!range.checkInRange(holder.getPosition())) {
+ throw new IllegalStateException(
+ "Invalid range specified --- does not contain drag target item"
+ + " (range = " + range + ", position = " + holder.getPosition() + ")");
+ }
+ }
+
+ private void handleActionMoveWhileDragging(RecyclerView rv, MotionEvent e) {
+ mLastTouchY = (int) (e.getY() + 0.5f);
+ mDragMinTouchY = Math.min(mDragMinTouchY, mLastTouchY);
+ mDragMaxTouchY = Math.max(mDragMaxTouchY, mLastTouchY);
+
+ // update drag direction mask
+ updateDragDirectionMask();
+
+ // update decorators
+ mDraggingItemDecorator.update(e);
+ if (mSwapTargetItemOperator != null) {
+ mSwapTargetItemOperator.update(mDraggingItemDecorator.getDraggingItemTranslationY());
+ }
+
+ // check swapping
+ checkItemSwapping(rv);
+ }
+
+ private void updateDragDirectionMask() {
+ if (((mDragStartTouchY - mDragMinTouchY) > mScrollTouchSlop) ||
+ ((mDragMaxTouchY - mLastTouchY) > mScrollTouchSlop)) {
+ mScrollDirMask |= SCROLL_DIR_UP;
+ }
+ if (((mDragMaxTouchY - mDragStartTouchY) > mScrollTouchSlop) ||
+ ((mLastTouchY - mDragMinTouchY) > mScrollTouchSlop)) {
+ mScrollDirMask |= SCROLL_DIR_DOWN;
+ }
+ }
+
+ private void checkItemSwapping(RecyclerView rv) {
+ final RecyclerView.ViewHolder draggingItem = mDraggingItem;
+
+ final int overlayItemTop = mLastTouchY - mGrabbedPositionY;
+ final RecyclerView.ViewHolder swapTargetHolder =
+ findSwapTargetItem(rv, draggingItem, mDraggingItemId, overlayItemTop, mDraggableRange);
+
+ if ((swapTargetHolder != null) && (swapTargetHolder != mDraggingItem)) {
+ swapItems(rv, draggingItem, swapTargetHolder);
+ }
+ }
+
+ /*package*/ void handleScrollOnDragging() {
+ final RecyclerView rv = mRecyclerView;
+ final int height = rv.getHeight();
+
+ if (height == 0) {
+ return;
+ }
+
+ final float invHeight = (1.0f / height);
+ final float y = mLastTouchY * invHeight;
+ final float threshold = SCROLL_THRESHOLD;
+ final float invThreshold = (1.0f / threshold);
+ final float centerOffset = y - 0.5f;
+ final float absCenterOffset = Math.abs(centerOffset);
+ final float acceleration = Math.max(0.0f, threshold - (0.5f - absCenterOffset)) * invThreshold;
+ final int mask = mScrollDirMask;
+
+ int scrollAmount = (int) Math.signum(centerOffset) * (int) (SCROLL_AMOUNT_COEFF * mDisplayDensity * acceleration + 0.5f);
+ int actualScrolledAmount = 0;
+
+ final ItemDraggableRange range = mDraggableRange;
+
+ final int firstVisibleChild = CustomRecyclerViewUtils.findFirstCompletelyVisibleItemPosition(mRecyclerView);
+ final int lastVisibleChild = CustomRecyclerViewUtils.findLastCompletelyVisibleItemPosition(mRecyclerView);
+
+ boolean reachedToTopHardLimit = false;
+ boolean reachedToTopSoftLimit = false;
+ boolean reachedToBottomHardLimit = false;
+ boolean reachedToBottomSoftLimit = false;
+
+ if (firstVisibleChild != RecyclerView.NO_POSITION) {
+ if (firstVisibleChild <= range.getStart()) {
+ reachedToTopSoftLimit = true;
+ }
+ if (firstVisibleChild <= (range.getStart() - 1)) {
+ reachedToTopHardLimit = true;
+ }
+ }
+
+ if (lastVisibleChild != RecyclerView.NO_POSITION) {
+ if (lastVisibleChild >= range.getEnd()) {
+ reachedToBottomSoftLimit = true;
+ }
+ if (lastVisibleChild >= (range.getEnd() + 1)) {
+ reachedToBottomHardLimit = true;
+ }
+ }
+
+ // apply mask
+ if (scrollAmount > 0) {
+ if ((mask & SCROLL_DIR_DOWN) == 0) {
+ scrollAmount = 0;
+ }
+ } else if (scrollAmount < 0) {
+ if ((mask & SCROLL_DIR_UP) == 0) {
+ scrollAmount = 0;
+ }
+ }
+
+ if ((!reachedToTopHardLimit && (scrollAmount < 0)) ||
+ (!reachedToBottomHardLimit && (scrollAmount > 0))) {
+ safeEndAnimations(rv);
+ actualScrolledAmount = scrollByYAndGetScrolledAmount(scrollAmount);
+
+ if (scrollAmount < 0) {
+ mDraggingItemDecorator.setIsScrolling(!reachedToTopSoftLimit);
+ } else {
+ mDraggingItemDecorator.setIsScrolling(!reachedToBottomSoftLimit);
+ }
+
+ mDraggingItemDecorator.refresh();
+ if (mSwapTargetItemOperator != null) {
+ mSwapTargetItemOperator.update(mDraggingItemDecorator.getDraggingItemTranslationY());
+ }
+ } else {
+ mDraggingItemDecorator.setIsScrolling(false);
+ }
+
+ final boolean actualIsScrolling = (actualScrolledAmount != 0);
+
+
+ if (mEdgeEffectDecorator != null) {
+ final int draggingItemTop = mDraggingItemDecorator.getTranslatedItemPositionTop();
+ final int draggingItemBottom = mDraggingItemDecorator.getTranslatedItemPositionBottom();
+ final int draggingItemCenter = (draggingItemTop + draggingItemBottom) / 2;
+ final int nearEdgePosition = ((draggingItemCenter < (height / 2)) ? draggingItemTop : draggingItemBottom);
+
+ final float nearEdgeOffset = (nearEdgePosition * invHeight) - 0.5f;
+ final float absNearEdgeOffset = Math.abs(nearEdgeOffset);
+ final float edgeEffectAcceleration = Math.max(0.0f, threshold - (0.5f - absNearEdgeOffset)) * invThreshold;
+
+ if ((edgeEffectAcceleration >= EDGE_EFFECT_THRESHOLD) && (scrollAmount != 0) && !actualIsScrolling) {
+ // over scrolled
+ final float distance = edgeEffectAcceleration * 0.02f;
+
+ if (scrollAmount < 0) {
+ // upward
+ mEdgeEffectDecorator.pullTopGlow(distance);
+ } else {
+ // downward
+ mEdgeEffectDecorator.pullBottom(distance);
+ }
+ } else {
+ mEdgeEffectDecorator.releaseBothGlows();
+ }
+ }
+
+ checkItemSwapping(rv);
+ }
+
+ private int scrollByYAndGetScrolledAmount(int ry) {
+ // NOTE: mActualScrollByAmount --- Hackish! To detect over scrolling.
+
+ mActualScrollByAmount = 0;
+ mInScrollByMethod = true;
+ mRecyclerView.scrollBy(0, ry);
+ mInScrollByMethod = false;
+
+ return mActualScrollByAmount;
+ }
+
+ /*package*/ RecyclerView getRecyclerView() {
+ return mRecyclerView;
+ }
+
+ private void startScrollOnDraggingProcess() {
+ mScrollOnDraggingProcess.start();
+ }
+
+ private void stopScrollOnDraggingProcess() {
+ if (mScrollOnDraggingProcess != null) {
+ mScrollOnDraggingProcess.stop();
+ }
+ }
+
+ private void swapItems(RecyclerView rv, RecyclerView.ViewHolder draggingItem, RecyclerView.ViewHolder swapTargetHolder) {
+ final Rect swapTargetMargins = CustomRecyclerViewUtils.getLayoutMargins(swapTargetHolder.itemView, mTmpRect1);
+ final int fromPosition = draggingItem.getPosition();
+ final int toPosition = swapTargetHolder.getPosition();
+ final int diffPosition = Math.abs(fromPosition - toPosition);
+ boolean performSwapping = false;
+
+ if (fromPosition == RecyclerView.NO_POSITION || toPosition == RecyclerView.NO_POSITION) {
+ return;
+ }
+
+ final long actualDraggingItemId = rv.getAdapter().getItemId(fromPosition);
+ if (actualDraggingItemId != mDraggingItemId) {
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "RecyclerView state has not been synched to data yet");
+ }
+ return;
+ }
+
+ //noinspection StatementWithEmptyBody
+ if (diffPosition == 0) {
+ } else if (diffPosition == 1) {
+ final View v1 = draggingItem.itemView;
+ final View v2 = swapTargetHolder.itemView;
+ final Rect m1 = mDraggingItemMargins;
+ //noinspection UnnecessaryLocalVariable
+ final Rect m2 = swapTargetMargins;
+
+ final int top = Math.min(v1.getTop() - m1.top, v2.getTop() - m2.top);
+ final int bottom = Math.max(v1.getBottom() + m1.bottom, v2.getBottom() + m2.bottom);
+
+ final float midPointOfTheItems = top + ((bottom - top) * 0.5f);
+ final float midPointOfTheOverlaidItem = (mLastTouchY - mGrabbedPositionY) + (mGrabbedItemHeight * 0.5f);
+
+ if (toPosition < fromPosition) {
+ if (midPointOfTheOverlaidItem < midPointOfTheItems) {
+ // swap (up direction)
+ performSwapping = true;
+ }
+ } else { // if (toPosition > fromPosition)
+ if (midPointOfTheOverlaidItem > midPointOfTheItems) {
+ // swap (down direction)
+ performSwapping = true;
+ }
+ }
+ } else { // diffPosition > 1
+ performSwapping = true;
+ }
+
+ if (performSwapping) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "item swap (from: " + fromPosition + ", to: " + toPosition + ")");
+ }
+
+ RecyclerView.ViewHolder firstVisibleTopItem = null;
+
+ if (rv.getChildCount() > 0) {
+ View child = rv.getChildAt(0);
+ if (child != null) {
+ firstVisibleTopItem = rv.getChildViewHolder(child);
+ }
+ }
+ final int prevTopItemPosition = (firstVisibleTopItem != null) ? firstVisibleTopItem.getPosition() : RecyclerView.NO_POSITION;
+
+
+ mAdapter.moveItem(fromPosition, toPosition);
+
+ if (fromPosition == prevTopItemPosition) {
+ //noinspection UnnecessaryLocalVariable
+ final Rect margins = swapTargetMargins;
+ final int curTopItemHeight = swapTargetHolder.itemView.getHeight() + margins.top + margins.bottom;
+ scrollByYAndGetScrolledAmount(-curTopItemHeight);
+ } else if (toPosition == prevTopItemPosition) {
+ final Rect margins = mDraggingItemMargins;
+ final int curTopItemHeight = mGrabbedItemHeight + margins.top + margins.bottom;
+ scrollByYAndGetScrolledAmount(-curTopItemHeight);
+ }
+
+ safeEndAnimations(rv);
+ }
+ }
+
+ private static DraggableItemWrapperAdapter getDraggableItemWrapperAdapter(RecyclerView rv) {
+ return WrapperAdapterUtils.findWrappedAdapter(rv.getAdapter(), DraggableItemWrapperAdapter.class);
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private boolean checkTouchedItemState(RecyclerView rv, RecyclerView.ViewHolder holder) {
+ if (!(holder instanceof DraggableItemViewHolder)) {
+ return false;
+ }
+
+ final int itemPosition = holder.getPosition();
+ final RecyclerView.Adapter adapter = rv.getAdapter();
+
+ // verify the touched item is valid state
+ if (!(itemPosition >= 0 && itemPosition < adapter.getItemCount())) {
+ return false;
+ }
+
+ //noinspection RedundantIfStatement
+ if (holder.getItemId() != adapter.getItemId(itemPosition)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean supportsEdgeEffect() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
+ }
+
+ private static boolean supportsViewTranslation() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
+ }
+
+ private static void safeEndAnimation(RecyclerView rv, RecyclerView.ViewHolder holder) {
+ final RecyclerView.ItemAnimator itemAnimator = (rv != null) ? rv.getItemAnimator() : null;
+ if (itemAnimator != null) {
+ itemAnimator.endAnimation(holder);
+ }
+ }
+
+ private static void safeEndAnimations(RecyclerView rv) {
+ final RecyclerView.ItemAnimator itemAnimator = (rv != null) ? rv.getItemAnimator() : null;
+ if (itemAnimator != null) {
+ itemAnimator.endAnimations();
+ }
+ }
+
+ /*package*/
+ static RecyclerView.ViewHolder findSwapTargetItem(
+ RecyclerView rv, RecyclerView.ViewHolder draggingItem,
+ long draggingItemId, int overlayItemTop, ItemDraggableRange range) {
+ final int draggingItemPosition = draggingItem.getPosition();
+ final int draggingViewTop = draggingItem.itemView.getTop();
+ RecyclerView.ViewHolder swapTargetHolder = null;
+
+ // determine the swap target view
+ if (draggingItemPosition != RecyclerView.NO_POSITION &&
+ draggingItem.getItemId() == draggingItemId) {
+ if (overlayItemTop < draggingViewTop) {
+ if (draggingItemPosition > 0) {
+ swapTargetHolder = rv.findViewHolderForPosition(draggingItemPosition - 1);
+ }
+ } else if (overlayItemTop > draggingViewTop) {
+ if (draggingItemPosition < (rv.getAdapter().getItemCount() - 1)) {
+ swapTargetHolder = rv.findViewHolderForPosition(draggingItemPosition + 1);
+ }
+ }
+ }
+
+ // check range
+ if (swapTargetHolder != null && range != null) {
+ if (!range.checkInRange(swapTargetHolder.getPosition())) {
+ swapTargetHolder = null;
+ }
+ }
+
+ return swapTargetHolder;
+ }
+
+ /**
+ * Sets the duration of "settle back into place" animation.
+ *
+ * @param duration Specify the animation duration in milliseconds
+ */
+ public void setItemSettleBackIntoPlaceAnimationDuration(int duration) {
+ mItemSettleBackIntoPlaceAnimationDuration = duration;
+ }
+
+ /**
+ * Gets the duration of "settle back into place" animation.
+ *
+ * @return The duration of "settle back into place" animation in milliseconds
+ */
+ public int getItemSettleBackIntoPlaceAnimationDuration() {
+ return mItemSettleBackIntoPlaceAnimationDuration;
+ }
+
+ /**
+ * Sets the interpolator which is used for "settle back into place" animation.
+ *
+ * @param interpolator Interpolator to set or null to clear
+ */
+ public void setItemSettleBackIntoPlaceAnimationInterpolator(Interpolator interpolator) {
+ mItemSettleBackIntoPlaceAnimationInterpolator = interpolator;
+ }
+
+ /**
+ * Gets the interpolator which ise used for "settle back into place" animation.
+ *
+ * @return Interpolator which is used for "settle back into place" animation
+ */
+ public Interpolator getItemSettleBackIntoPlaceAnimationInterpolator() {
+ return mItemSettleBackIntoPlaceAnimationInterpolator;
+ }
+
+ private static class ScrollOnDraggingProcessRunnable implements Runnable {
+ private final WeakReference mHolderRef;
+ private boolean mStarted;
+
+ public ScrollOnDraggingProcessRunnable(RecyclerViewDragDropManager holder) {
+ mHolderRef = new WeakReference<>(holder);
+ }
+
+ public void start() {
+ if (mStarted) {
+ return;
+ }
+
+ final RecyclerViewDragDropManager holder = mHolderRef.get();
+
+ if (holder == null) {
+ return;
+ }
+
+ final RecyclerView rv = holder.getRecyclerView();
+
+ if (rv == null) {
+ return;
+ }
+
+ ViewCompat.postOnAnimation(rv, this);
+
+ mStarted = true;
+ }
+
+ public void stop() {
+ if (!mStarted) {
+ return;
+ }
+
+ mStarted = false;
+ }
+
+ public void release() {
+ mHolderRef.clear();
+ mStarted = false;
+ }
+
+ @Override
+ public void run() {
+ final RecyclerViewDragDropManager holder = mHolderRef.get();
+
+ if (holder == null) {
+ return;
+ }
+
+ if (!mStarted) {
+ return;
+ }
+
+ // call scrolling process
+ holder.handleScrollOnDragging();
+
+ // re-schedule the process
+ final RecyclerView rv = holder.getRecyclerView();
+
+ if (rv != null && mStarted) {
+ ViewCompat.postOnAnimation(rv, this);
+ } else {
+ mStarted = false;
+ }
+ }
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/SwapTargetItemOperator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/SwapTargetItemOperator.java
new file mode 100644
index 00000000000..66e58c69121
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/SwapTargetItemOperator.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.draggable;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.h6ah4i.android.widget.advrecyclerview.utils.CustomRecyclerViewUtils;
+
+class SwapTargetItemOperator extends BaseDraggableItemDecorator {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SwapTargetItemOperator";
+
+ private RecyclerView.ViewHolder mSwapTargetItem;
+ private Interpolator mSwapTargetTranslationInterpolator;
+ private int mTranslationY;
+ private int mDraggingItemHeight;
+ private Rect mSwapTargetDecorationOffsets = new Rect();
+ private Rect mSwapTargetItemMargins = new Rect();
+ private Rect mDraggingItemMargins = new Rect();
+ private Rect mDraggingItemDecorationOffsets = new Rect();
+ private boolean mStarted;
+ private float mReqTranslationPhase;
+ private float mCurTranslationPhase;
+ private long mDraggingItemId;
+ private ItemDraggableRange mRange;
+
+ public SwapTargetItemOperator(RecyclerView recyclerView, RecyclerView.ViewHolder draggingItem, ItemDraggableRange range) {
+ super(recyclerView, draggingItem);
+
+ mDraggingItemId = mDraggingItem.getItemId();
+ mRange = range;
+
+ CustomRecyclerViewUtils.getLayoutMargins(mDraggingItem.itemView, mDraggingItemMargins);
+ CustomRecyclerViewUtils.getDecorationOffsets(
+ mRecyclerView.getLayoutManager(), mDraggingItem.itemView, mDraggingItemDecorationOffsets);
+ }
+
+ public void setSwapTargetTranslationInterpolator(Interpolator interpolator) {
+ mSwapTargetTranslationInterpolator = interpolator;
+ }
+
+ @Override
+ public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ final RecyclerView.ViewHolder draggingItem = mDraggingItem;
+
+ if (draggingItem == null || draggingItem.getItemId() != mDraggingItemId) {
+ return;
+ }
+
+ final RecyclerView.ViewHolder swapTargetItem =
+ RecyclerViewDragDropManager.findSwapTargetItem(
+ mRecyclerView, draggingItem, mDraggingItemId, mTranslationY, mRange);
+
+ // reset Y-translation if the swap target has changed
+ if ((mSwapTargetItem != swapTargetItem) && (mSwapTargetItem != null)) {
+ setItemTranslationY(mRecyclerView, mSwapTargetItem, 0.0f);
+ }
+
+ if (swapTargetItem != null) {
+ mReqTranslationPhase = calculateTranslationPhase(draggingItem, swapTargetItem);
+
+ if (mSwapTargetItem != swapTargetItem) {
+ mCurTranslationPhase = mReqTranslationPhase;
+ } else {
+ // interpolate to make it moves smoothly
+ mCurTranslationPhase = calculateCurrentTranslationPhase(mCurTranslationPhase, mReqTranslationPhase);
+ }
+
+ updateSwapTargetTranslation(draggingItem, swapTargetItem, mCurTranslationPhase);
+ }
+
+ mSwapTargetItem = swapTargetItem;
+ }
+
+ private float calculateTranslationPhase(RecyclerView.ViewHolder draggingItem, RecyclerView.ViewHolder swapTargetItem) {
+ final View swapItemView = swapTargetItem.itemView;
+
+ final int pos1 = draggingItem.getPosition();
+ final int pos2 = swapTargetItem.getPosition();
+
+ CustomRecyclerViewUtils.getDecorationOffsets(
+ mRecyclerView.getLayoutManager(), swapItemView, mSwapTargetDecorationOffsets);
+ CustomRecyclerViewUtils.getLayoutMargins(swapItemView, mSwapTargetItemMargins);
+
+ final Rect m2 = mSwapTargetItemMargins;
+ final Rect d2 = mSwapTargetDecorationOffsets;
+ final int h2 = swapItemView.getHeight() + m2.top + m2.bottom + d2.top + d2.bottom;
+
+ final float offsetPx = draggingItem.itemView.getTop() - mTranslationY; // == -(ViewCompat.getTranslationY(draggingItem.itemView)
+ final float phase = (h2 != 0) ? (offsetPx / h2) : 0.0f;
+
+ float translationPhase;
+
+ if (pos1 > pos2) {
+ // dragging item moving to upward
+ translationPhase = phase;
+ } else {
+ // dragging item moving to downward
+ translationPhase = 1.0f + phase;
+ }
+
+ return Math.min(Math.max(translationPhase, 0.0f), 1.0f);
+ }
+
+ private static float calculateCurrentTranslationPhase(float cur, float req) {
+ final float A = 0.3f;
+ final float B = 0.01f;
+ final float tmp = (cur * (1.0f - A)) + (req * A);
+
+ return (Math.abs(tmp - req) < B) ? req : tmp;
+ }
+
+ private void updateSwapTargetTranslation(RecyclerView.ViewHolder draggingItem, RecyclerView.ViewHolder swapTargetItem, float translationPhase) {
+ final View swapItemView = swapTargetItem.itemView;
+
+ final int pos1 = draggingItem.getPosition();
+ final int pos2 = swapTargetItem.getPosition();
+
+ final Rect m1 = mDraggingItemMargins;
+ final Rect d1 = mDraggingItemDecorationOffsets;
+ final int h1 = mDraggingItemHeight + m1.top + m1.bottom + d1.top + d1.bottom;
+
+ if (mSwapTargetTranslationInterpolator != null) {
+ translationPhase = mSwapTargetTranslationInterpolator.getInterpolation(translationPhase);
+ }
+
+ if (pos1 > pos2) {
+ // dragging item moving to upward
+ ViewCompat.setTranslationY(swapItemView, translationPhase * h1);
+ } else {
+ // dragging item moving to downward
+ ViewCompat.setTranslationY(swapItemView, (translationPhase - 1.0f) * h1);
+ }
+ }
+
+ public void start() {
+ if (mStarted) {
+ return;
+ }
+
+ mDraggingItemHeight = mDraggingItem.itemView.getHeight();
+
+ mRecyclerView.addItemDecoration(this, 0);
+
+ mStarted = true;
+ }
+
+ public void finish(boolean animate) {
+ if (mStarted) {
+ mRecyclerView.removeItemDecoration(this);
+ }
+
+ final RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator();
+ if (itemAnimator != null) {
+ itemAnimator.endAnimations();
+ }
+ mRecyclerView.stopScroll();
+
+ if (mSwapTargetItem != null) {
+ // return to default position
+ updateSwapTargetTranslation(mDraggingItem, mSwapTargetItem, mCurTranslationPhase);
+ moveToDefaultPosition(mSwapTargetItem.itemView, animate);
+ mSwapTargetItem = null;
+ }
+
+ mRange = null;
+ mDraggingItem = null;
+ mTranslationY = 0;
+ mDraggingItemHeight = 0;
+ mCurTranslationPhase = 0.0f;
+ mReqTranslationPhase = 0.0f;
+ mStarted = false;
+ }
+
+ public void update(int translationY) {
+ mTranslationY = translationY;
+ }
+}
\ No newline at end of file
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/BaseRecyclerViewEventDistributor.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/BaseRecyclerViewEventDistributor.java
new file mode 100644
index 00000000000..a7f8d022a72
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/BaseRecyclerViewEventDistributor.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.event;
+
+import android.support.v7.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class BaseRecyclerViewEventDistributor {
+ protected boolean mReleased;
+ protected RecyclerView mRecyclerView;
+ protected List mListeners;
+ protected boolean mPerformingClearMethod;
+
+ public BaseRecyclerViewEventDistributor() {
+ }
+
+ /**
+ * Gets attached {@link android.support.v7.widget.RecyclerView}
+ *
+ * @return The {@link android.support.v7.widget.RecyclerView} instance
+ */
+ public RecyclerView getRecyclerView() {
+ return mRecyclerView;
+ }
+
+ /**
+ * Release all references.
+ * This method should be called to avoid memory leaks.
+ */
+ public void release() {
+ if (mReleased) {
+ return;
+ }
+ mReleased = true;
+ clear(true);
+ onRelease();
+ }
+
+ /**
+ * Indicates whether this distributor has been released
+ *
+ * @return True for this distributor has been released, otherwise false
+ */
+ public boolean isReleased() {
+ return mReleased;
+ }
+
+ /**
+ * Attaches {@link android.support.v7.widget.RecyclerView} instance.
+ *
+ * @param rv The {@link android.support.v7.widget.RecyclerView} instance
+ */
+ public void attachRecyclerView(RecyclerView rv) {
+ final String METHOD_NAME = "attachRecyclerView()";
+
+ if (rv == null) {
+ throw new IllegalArgumentException("RecyclerView cannot be null");
+ }
+
+ verifyIsNotReleased(METHOD_NAME);
+ verifyIsNotPerformingClearMethod(METHOD_NAME);
+
+ onRecyclerViewAttached(rv);
+ }
+
+ /**
+ * Add a {@link T} listener to the chain.
+ *
+ * @param listener The {@link T} instance
+ *
+ * @return True if the listener object successfully added, otherwise false. Also returns true if have already been added.
+ */
+ public boolean add(T listener) {
+ return add(listener, -1);
+ }
+
+ /**
+ * Add a {@link T} listener to the chain at the specified position.
+ *
+ * @param listener The {@link T} instance
+ * @param index Position in the listener chain to insert this listener at. (< 0: tail of the chain)
+ *
+ * @return True if the listener object successfully added, otherwise false. Also returns true if have already been added.
+ */
+ public boolean add(T listener, int index) {
+ final String METHOD_NAME = "add()";
+
+ if (listener == null) {
+ throw new IllegalArgumentException("can not specify null for the listener");
+ }
+
+ verifyIsNotReleased(METHOD_NAME);
+ verifyIsNotPerformingClearMethod(METHOD_NAME);
+
+ if (mListeners == null) {
+ mListeners = new ArrayList<>();
+ }
+
+ if (!mListeners.contains(listener)) {
+ if (index < 0) {
+ // append to the tail of the list
+ mListeners.add(listener);
+ } else {
+ // insert to the specified position
+ mListeners.add(index, listener);
+ }
+
+ // raise onAddedToEventDistributor() event
+ if (listener instanceof RecyclerViewEventDistributorListener) {
+ ((RecyclerViewEventDistributorListener) listener).onAddedToEventDistributor(this);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Remove a {@link T} listener from the chain.
+ *
+ * @param listener Listener to remove
+ *
+ * @return True for successfully removed the listener object, otherwise false
+ */
+ public boolean remove(T listener) {
+ final String METHOD_NAME = "remove()";
+
+ if (listener == null) {
+ throw new IllegalArgumentException("can not specify null for the listener");
+ }
+
+ verifyIsNotPerformingClearMethod(METHOD_NAME);
+ verifyIsNotReleased(METHOD_NAME);
+
+ if (mListeners == null) {
+ return false;
+ }
+
+ final boolean removed = mListeners.remove(listener);
+
+ if (removed) {
+ // raise onRemovedFromEventDistributor() event
+ if (listener instanceof RecyclerViewEventDistributorListener) {
+ ((RecyclerViewEventDistributorListener) listener).onRemovedFromEventDistributor(this);
+ }
+ }
+
+ return removed;
+ }
+
+ /**
+ * Remove all listeners from the chain.
+ */
+ public void clear() {
+ clear(false);
+ }
+
+ protected void clear(boolean calledFromRelease) {
+ final String METHOD_NAME = "clear()";
+
+ if (!calledFromRelease) {
+ verifyIsNotReleased(METHOD_NAME);
+ }
+ verifyIsNotPerformingClearMethod(METHOD_NAME);
+
+ if (mListeners == null) {
+ return;
+ }
+
+ try {
+ mPerformingClearMethod = true;
+
+ final int n = mListeners.size();
+
+ for (int i = n - 1; i >= 0; i--) {
+ final T listener = mListeners.remove(i);
+
+ // raise onRemovedFromEventDistributor() event
+ if (listener instanceof RecyclerViewEventDistributorListener) {
+ ((RecyclerViewEventDistributorListener) listener).onRemovedFromEventDistributor(this);
+ }
+ }
+ } finally {
+ mPerformingClearMethod = false;
+ }
+ }
+
+ /**
+ * Gets the number of underlying listener objects.
+ *
+ * @return Number of underlying listener objects in the chain.
+ */
+ public int size() {
+ if (mListeners != null) {
+ return mListeners.size();
+ } else {
+ return mListeners.size();
+ }
+ }
+
+ /**
+ * Gets whether the specified listener object is contained in the chain.
+ *
+ * @param listener Listener to check
+ *
+ * @return True for the listener contains in the chain, otherwise false
+ */
+ public boolean contains(T listener) {
+ if (mListeners != null) {
+ return mListeners.contains(listener);
+ } else {
+ return false;
+ }
+ }
+
+ protected void onRelease() {
+ mRecyclerView = null;
+ mListeners = null;
+ mPerformingClearMethod = false;
+ }
+
+ protected void onRecyclerViewAttached(RecyclerView rv) {
+ mRecyclerView = rv;
+ }
+
+ protected void verifyIsNotPerformingClearMethod(String methodName) {
+ if (mPerformingClearMethod) {
+ throw new IllegalStateException(methodName + " can not be called while performing the clear() method");
+ }
+ }
+
+ protected void verifyIsNotReleased(String methodName) {
+ if (mReleased) {
+ throw new IllegalStateException(methodName + " can not be called after release() method called");
+ }
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewEventDistributorListener.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewEventDistributorListener.java
new file mode 100644
index 00000000000..437b7dc0377
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewEventDistributorListener.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.event;
+
+public interface RecyclerViewEventDistributorListener {
+ void onAddedToEventDistributor(BaseRecyclerViewEventDistributor distributor);
+ void onRemovedFromEventDistributor(BaseRecyclerViewEventDistributor distributor);
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewOnScrollEventDistributor.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewOnScrollEventDistributor.java
new file mode 100644
index 00000000000..fb9ff5d79d9
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewOnScrollEventDistributor.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.event;
+
+import android.support.v7.widget.RecyclerView;
+
+import java.lang.ref.WeakReference;
+
+public class RecyclerViewOnScrollEventDistributor extends BaseRecyclerViewEventDistributor {
+
+ private InternalOnScrollListener mInternalOnScrollListener;
+
+ public RecyclerViewOnScrollEventDistributor() {
+ super();
+
+ mInternalOnScrollListener = new InternalOnScrollListener(this);
+ }
+
+ @Override
+ protected void onRecyclerViewAttached(RecyclerView rv) {
+ super.onRecyclerViewAttached(rv);
+
+ rv.setOnScrollListener(mInternalOnScrollListener);
+ }
+
+ @Override
+ protected void onRelease() {
+ super.onRelease();
+
+ if (mInternalOnScrollListener != null) {
+ mInternalOnScrollListener.release();
+ mInternalOnScrollListener = null;
+ }
+ }
+
+ /*package*/ void handleOnScrollStateChanged(RecyclerView recyclerView, int newState) {
+ if (mListeners == null) {
+ return;
+ }
+
+ for (RecyclerView.OnScrollListener listener : mListeners) {
+ listener.onScrollStateChanged(recyclerView, newState);
+ }
+ }
+
+ /*package*/ void handleOnScrolled(RecyclerView recyclerView, int dx, int dy) {
+ if (mListeners == null) {
+ return;
+ }
+
+ for (RecyclerView.OnScrollListener listener : mListeners) {
+ listener.onScrolled(recyclerView, dx, dy);
+ }
+ }
+
+ private static class InternalOnScrollListener extends RecyclerView.OnScrollListener {
+ private WeakReference mRefDistributor;
+
+ public InternalOnScrollListener(RecyclerViewOnScrollEventDistributor distributor) {
+ super();
+ mRefDistributor = new WeakReference<>(distributor);
+ }
+
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ final RecyclerViewOnScrollEventDistributor distributor = mRefDistributor.get();
+ if (distributor != null) {
+ distributor.handleOnScrollStateChanged(recyclerView, newState);
+ }
+ }
+
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ final RecyclerViewOnScrollEventDistributor holder = mRefDistributor.get();
+ if (holder != null) {
+ holder.handleOnScrolled(recyclerView, dx, dy);
+ }
+ }
+
+ public void release() {
+ mRefDistributor.clear();
+ }
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewRecyclerEventDistributor.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewRecyclerEventDistributor.java
new file mode 100644
index 00000000000..d88870637f3
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewRecyclerEventDistributor.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.event;
+
+import android.support.v7.widget.RecyclerView;
+
+import java.lang.ref.WeakReference;
+
+public class RecyclerViewRecyclerEventDistributor extends BaseRecyclerViewEventDistributor {
+
+ private InternalRecyclerListener mInternalRecyclerListener;
+
+ public RecyclerViewRecyclerEventDistributor() {
+ super();
+
+ mInternalRecyclerListener = new InternalRecyclerListener(this);
+ }
+
+ @Override
+ protected void onRecyclerViewAttached(RecyclerView rv) {
+ super.onRecyclerViewAttached(rv);
+
+ rv.setRecyclerListener(mInternalRecyclerListener);
+ }
+
+ @Override
+ protected void onRelease() {
+ super.onRelease();
+
+ if (mInternalRecyclerListener != null) {
+ mInternalRecyclerListener.release();
+ mInternalRecyclerListener = null;
+ }
+ }
+
+ /*package*/ void handleOnViewRecycled(RecyclerView.ViewHolder holder) {
+ if (mListeners == null) {
+ return;
+ }
+
+ for (RecyclerView.RecyclerListener listener : mListeners) {
+ listener.onViewRecycled(holder);
+ }
+ }
+
+ private static class InternalRecyclerListener implements RecyclerView.RecyclerListener {
+ private WeakReference mRefDistributor;
+
+ public InternalRecyclerListener(RecyclerViewRecyclerEventDistributor distributor) {
+ super();
+ mRefDistributor = new WeakReference<>(distributor);
+ }
+
+ @Override
+ public void onViewRecycled(RecyclerView.ViewHolder holder) {
+ final RecyclerViewRecyclerEventDistributor distributor = mRefDistributor.get();
+
+ if (distributor != null) {
+ distributor.handleOnViewRecycled(holder);
+ }
+ }
+
+ public void release() {
+ mRefDistributor.clear();
+ }
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableAdapterHelper.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableAdapterHelper.java
new file mode 100644
index 00000000000..e1db29130c5
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableAdapterHelper.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.expandable;
+
+import android.support.v7.widget.RecyclerView;
+
+class ExpandableAdapterHelper {
+ public static final long NO_EXPANDABLE_POSITION = 0xffffffffffffffffl;
+
+ private static final long LOWER_32BIT_MASK = 0x00000000ffffffffl;
+ private static final long LOWER_31BIT_MASK = 0x000000007fffffffl;
+
+ /*package*/ static final int VIEW_TYPE_FLAG_IS_GROUP = 0x80000000;
+
+ public static long getPackedPositionForChild(int groupPosition, int childPosition) {
+ return ((long) childPosition << 32) | (groupPosition & LOWER_32BIT_MASK);
+ }
+
+ public static long getPackedPositionForGroup(int groupPosition) {
+ return ((long) RecyclerView.NO_POSITION << 32) | (groupPosition & LOWER_32BIT_MASK);
+ }
+
+ public static int getPackedPositionChild(long packedPosition) {
+ return (int) (packedPosition >>> 32);
+ }
+
+ public static int getPackedPositionGroup(long packedPosition) {
+ return (int) (packedPosition & LOWER_32BIT_MASK);
+ }
+
+ public static long getCombinedChildId(long groupId, long childId) {
+ return ((groupId & LOWER_31BIT_MASK) << 32) | (childId & LOWER_32BIT_MASK);
+ }
+
+ public static long getCombinedGroupId(long groupId) {
+ //noinspection PointlessBitwiseExpression
+ return ((groupId & LOWER_31BIT_MASK) << 32) | (RecyclerView.NO_ID & LOWER_32BIT_MASK);
+ }
+
+ public static boolean isGroupViewType(int rawViewType) {
+ return ((rawViewType & VIEW_TYPE_FLAG_IS_GROUP) != 0);
+ }
+
+ public static int getGroupViewType(int rawViewType) {
+ return (rawViewType & (~VIEW_TYPE_FLAG_IS_GROUP));
+ }
+
+ public static int getChildViewType(int rawViewType) {
+ return (rawViewType & (~VIEW_TYPE_FLAG_IS_GROUP));
+ }
+
+ private ExpandableAdapterHelper() {
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableDraggableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableDraggableItemAdapter.java
new file mode 100644
index 00000000000..1e5d5f368c3
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableDraggableItemAdapter.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.expandable;
+
+import android.support.v7.widget.RecyclerView;
+
+import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange;
+
+public interface ExpandableDraggableItemAdapter {
+ /**
+ * Called when user is attempt to drag the group item.
+ *
+ * @param holder The group ViewHolder which is associated to item user is attempt to start dragging.
+ * @param groupPosition Group position.
+ * @param x Touched X position. Relative from the itemView's top-left.
+ * @param y Touched Y position. Relative from the itemView's top-left.
+ *
+ * @return Whether can start dragging.
+ */
+ boolean onCheckGroupCanStartDrag(GVH holder, int groupPosition, int x, int y);
+
+ /**
+ * Called when user is attempt to drag the child item.
+ *
+ * @param holder The child ViewHolder which is associated to item user is attempt to start dragging.
+ * @param groupPosition Group position.
+ * @param childPosition Child position.
+ * @param x Touched X position. Relative from the itemView's top-left.
+ * @param y Touched Y position. Relative from the itemView's top-left.
+ *
+ * @return Whether can start dragging.
+ */
+ boolean onCheckChildCanStartDrag(CVH holder, int groupPosition, int childPosition, int x, int y);
+
+ /**
+ * Called after the {@link #onCheckGroupCanStartDrag(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method returned true.
+ *
+ * @param holder The ViewHolder which is associated to item user is attempt to start dragging.
+ * @param groupPosition Group position.
+ *
+ * @return null: no constraints (= new ItemDraggableRange(0, getGroupCount() - 1)),
+ * otherwise: the range specified item can be drag-sortable.
+ */
+ ItemDraggableRange onGetGroupItemDraggableRange(GVH holder, int groupPosition);
+
+ /**
+ * Called after the {@link #onCheckChildCanStartDrag(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int, int)} method returned true.
+ *
+ * @param holder The ViewHolder which is associated to item user is attempt to start dragging.
+ * @param groupPosition Group position.
+ * @param childPosition Child position.
+ *
+ * @return null: no constraints (= new ItemDraggableRange(0, getGroupCount() - 1)),
+ * otherwise: the range specified item can be drag-sortable.
+ */
+ ItemDraggableRange onGetChildItemDraggableRange(CVH holder, int groupPosition, int childPosition);
+
+ /**
+ * Called when group item is moved. Should apply the move operation result to data set.
+ *
+ * @param fromGroupPosition Previous group position of the item.
+ * @param toGroupPosition New group position of the item.
+ */
+ void onMoveGroupItem(int fromGroupPosition, int toGroupPosition);
+
+ /**
+ * Called when child item is moved. Should apply the move operation result to data set.
+ *
+ * @param fromGroupPosition Previous group position of the item.
+ * @param fromChildPosition Previous child position of the item.
+ * @param toGroupPosition New group position of the item.
+ * @param toChildPosition New child position of the item.
+ */
+ void onMoveChildItem(int fromGroupPosition, int fromChildPosition, int toGroupPosition, int toChildPosition);
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemAdapter.java
new file mode 100644
index 00000000000..cc8cef577c8
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemAdapter.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.expandable;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.ViewGroup;
+
+public interface ExpandableItemAdapter {
+
+ /**
+ * Gets the number of groups.
+ *
+ * @return the number of groups
+ */
+ int getGroupCount();
+
+ /**
+ * Gets the number of children in a specified group.
+ *
+ * @param groupPosition the position of the group for which the children count should be returned
+ *
+ * @return the number of children
+ */
+ int getChildCount(int groupPosition);
+
+ /**
+ * Gets the ID for the group at the given position. This group ID must be unique across groups.
+ *
+ * The combined ID (see {@link RecyclerViewExpandableItemManager#getCombinedGroupId(long)})
+ * must be unique across ALL items (groups and all children).
+ *
+ * @param groupPosition the position of the group for which the ID is wanted
+ * @return the ID associated with the group
+ */
+ long getGroupId(int groupPosition);
+
+ /**
+ * Gets the ID for the given child within the given group.
+ *
+ * This ID must be unique across all children within the group.
+ * The combined ID (see {@link RecyclerViewExpandableItemManager#getCombinedChildId(long, long)})
+ * must be unique across ALL items (groups and all children).
+ *
+ * @param groupPosition the position of the group that contains the child
+ * @param childPosition the position of the child within the group for which the ID is wanted
+
+ * @return the ID associated with the child
+ */
+ long getChildId(int groupPosition, int childPosition);
+
+ /**
+ * Gets the view type of the specified group.
+ *
+ * @param groupPosition the position of the group for which the view type is wanted
+ *
+ * @return integer value identifying the type of the view needed to represent the group item at position. Type codes need positive number but not be contiguous.
+ */
+ int getGroupItemViewType(int groupPosition);
+
+ /**
+ * Gets the view type of the specified child.
+ *
+ * @param groupPosition the position of the group that contains the child
+ * @param childPosition the position of the child within the group for which the view type is wanted
+ *
+ * @return integer value identifying the type of the view needed to represent the group item at position. Type codes need positive number but not be contiguous.
+ */
+ int getChildItemViewType(int groupPosition, int childPosition);
+
+ /**
+ * Called when RecyclerView needs a new {@link GVH} of the given type to represent a group item.
+ *
+ * @param parent The ViewGroup into which the new View will be added after it is bound to an adapter position
+ * @param viewType The view type of the new View
+ *
+ * @return A new group ViewHolder that holds a View of the given view type
+ */
+ GVH onCreateGroupViewHolder(ViewGroup parent, int viewType);
+
+ /**
+ * Called when RecyclerView needs a new {@link CVH} of the given type to represent a child item.
+ *
+ * @param parent The ViewGroup into which the new View will be added after it is bound to an adapter position
+ * @param viewType The view type of the new View
+ *
+ * @return A new child ViewHolder that holds a View of the given view type
+ */
+ CVH onCreateChildViewHolder(ViewGroup parent, int viewType);
+
+ /**
+ * Called by RecyclerView to display the group data at the specified position.
+ * This method should update the contents of the {@link android.support.v7.widget.RecyclerView.ViewHolder#itemView}
+ * to reflect the item at the given position.
+ *
+ * @param holder The ViewHolder which should be updated to represent the contents of the item at the given position in the data set
+ * @param groupPosition The position of the group item within the adapter's data set
+ * @param viewType The view type code
+ */
+ void onBindGroupViewHolder(GVH holder, int groupPosition, int viewType);
+
+ /**
+ * Called by RecyclerView to display the child data at the specified position.
+ * This method should update the contents of the {@link android.support.v7.widget.RecyclerView.ViewHolder#itemView}
+ * to reflect the item at the given position.
+ *
+ * @param holder The ViewHolder which should be updated to represent the contents of the item at the given position in the data set
+ * @param groupPosition The position of the group item within the adapter's data set
+ * @param childPosition The position of the child item within the group
+ * @param viewType The view type code
+ */
+ void onBindChildViewHolder(CVH holder, int groupPosition, int childPosition, int viewType);
+
+ /**
+ * Called when the user attempt to expand/collapse the group item.
+ *
+ * @param holder The ViewHolder which is associated to group item user is attempt to expand/collapse
+ * @param groupPosition Group position
+ * @param x Touched X position. Relative from the itemView's top-left
+ * @param y Touched Y position. Relative from the itemView's top-left
+ * @param expand true: expand, false: collapse
+ *
+ * @return Whether to perform expand/collapse operation.
+ */
+ boolean onCheckCanExpandOrCollapseGroup(GVH holder, int groupPosition, int x, int y, boolean expand);
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemViewHolder.java
new file mode 100644
index 00000000000..13803d4c6a8
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemViewHolder.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.expandable;
+
+/**
+ * Interface which provides required information for expanding item.
+ *
+ * Implement this interface on your sub-class of the {@link android.support.v7.widget.RecyclerView.ViewHolder}.
+ */
+public interface ExpandableItemViewHolder {
+ /**
+ * Sets the state flags value for expanding item
+ *
+ * @param flags Bitwise OR of these flags;
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_GROUP}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_CHILD}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_EXPANDED}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_UPDATED}
+ */
+ void setExpandStateFlags(int flags);
+
+ /**
+ * Gets the state flags value for expanding item
+ *
+ * @return Bitwise OR of these flags;
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_GROUP}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_CHILD}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_EXPANDED}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.expandable.RecyclerViewExpandableItemManager#STATE_FLAG_IS_UPDATED}
+ */
+ int getExpandStateFlags();
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandablePositionTranslator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandablePositionTranslator.java
new file mode 100644
index 00000000000..edd5e1db8a4
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandablePositionTranslator.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.expandable;
+
+import android.support.v7.widget.RecyclerView;
+
+import java.util.Arrays;
+
+class ExpandablePositionTranslator {
+ private final static long FLAG_EXPANDED = 0x0000000080000000l;
+ private final static long LOWER_31BIT_MASK = 0x000000007fffffffl;
+ private final static long LOWER_32BIT_MASK = 0x00000000ffffffffl;
+ private final static long UPPER_32BIT_MASK = 0xffffffff00000000l;
+
+ /*
+ * bit 64-32: offset (use for caching purpose)
+ * bit 31: expanded or not
+ * bit 30-0: child count
+ */
+ private long[] mCachedGroupPosInfo;
+
+ /*
+ * bit 31: reserved
+ * bit 30-0: group id
+ */
+ private int[] mCachedGroupId;
+ private int mGroupCount;
+ private int mExpandedGroupCount;
+ private int mExpandedChildCount;
+ private int mEndOfCalculatedOffsetGroupPosition = RecyclerView.NO_POSITION;
+
+ public ExpandablePositionTranslator() {
+ }
+
+ public void build(ExpandableItemAdapter adapter) {
+ final int groupCount = adapter.getGroupCount();
+
+ if (mCachedGroupPosInfo == null || mCachedGroupPosInfo.length < groupCount) {
+ mCachedGroupPosInfo = new long[groupCount];
+ }
+ if (mCachedGroupId == null || mCachedGroupId.length < groupCount) {
+ mCachedGroupId = new int[groupCount];
+ }
+
+ final long[] info = mCachedGroupPosInfo;
+ final int[] ids = mCachedGroupId;
+ for (int i = 0; i < groupCount; i++) {
+ final long groupId = adapter.getGroupId(i);
+ final int childCount = adapter.getChildCount(i);
+
+ info[i] = (((long) i << 32) | childCount);
+ ids[i] = (int) (groupId & LOWER_32BIT_MASK);
+ }
+
+ mGroupCount = groupCount;
+ mExpandedGroupCount = 0;
+ mExpandedChildCount = 0;
+ mEndOfCalculatedOffsetGroupPosition = Math.max(0, groupCount - 1);
+ }
+
+ public void restoreExpandedGroupItems(int[] restoreGroupIds) {
+ if (restoreGroupIds == null || restoreGroupIds.length == 0) {
+ return;
+ }
+
+ if (mCachedGroupPosInfo == null) {
+ return;
+ }
+
+ // make ID + position packed array
+ final long [] idAndPos = new long[mGroupCount];
+
+ for (int i = 0; i < mGroupCount; i++) {
+ idAndPos[i] = ((long) mCachedGroupId[i] << 32) | i;
+ }
+
+ // sort both arrays
+ Arrays.sort(restoreGroupIds);
+ Arrays.sort(idAndPos);
+
+ // find matched items & apply
+ int index = 0;
+ //noinspection ForLoopReplaceableByForEach
+ for (int i = 0; i < restoreGroupIds.length; i++) {
+ final int id1 = restoreGroupIds[i];
+
+ for (int j = index; j < idAndPos.length; j++) {
+ final int id2 = (int) (idAndPos[j] >> 32);
+
+ if (id2 < id1) {
+ index = j;
+ } else if (id2 == id1) {
+ // matched
+ final int position = (int) (idAndPos[j] & LOWER_31BIT_MASK);
+ index = j + 1;
+ expandGroup(position);
+ break;
+ } else { // id2 > id1
+ break;
+ }
+ }
+ }
+ }
+
+ public int[] getSavedStateArray() {
+ int [] expandedGroups = new int[mExpandedGroupCount];
+
+ int index = 0;
+ for (int i = 0; i < mGroupCount; i++) {
+ final long t = mCachedGroupPosInfo[i];
+ if ((t & FLAG_EXPANDED) != 0) {
+ expandedGroups[index] = mCachedGroupId[i];
+ index += 1;
+ }
+ }
+
+ if (index != mExpandedGroupCount) {
+ throw new IllegalStateException("may be a bug (index = " + index + ", mExpandedGroupCount = " + mExpandedGroupCount + ")");
+ }
+
+ return expandedGroups;
+ }
+
+ public int getItemCount() {
+ return mGroupCount + mExpandedChildCount;
+ }
+
+ public boolean isGroupExpanded(int groupPosition) {
+ return ((mCachedGroupPosInfo[groupPosition] & FLAG_EXPANDED) != 0);
+ }
+
+ public int getChildCount(int groupPosition) {
+ return (int) (mCachedGroupPosInfo[groupPosition] & LOWER_31BIT_MASK);
+ }
+
+ public int getVisibleChildCount(int groupPosition) {
+ if (isGroupExpanded(groupPosition)) {
+ return getChildCount(groupPosition);
+ } else {
+ return 0;
+ }
+ }
+
+ public boolean collapseGroup(int groupPosition) {
+ if ((mCachedGroupPosInfo[groupPosition] & FLAG_EXPANDED) == 0) {
+ return false;
+ }
+
+ final int childCount = (int) (mCachedGroupPosInfo[groupPosition] & LOWER_31BIT_MASK);
+
+ mCachedGroupPosInfo[groupPosition] &= (~FLAG_EXPANDED);
+ mExpandedGroupCount -= 1;
+
+ if (childCount > 0) {
+ mExpandedChildCount -= childCount;
+ mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition);
+ // requires notifyItemRangeRemoved()
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean expandGroup(int groupPosition) {
+ if ((mCachedGroupPosInfo[groupPosition] & FLAG_EXPANDED) != 0) {
+ return false;
+ }
+
+ final int childCount = (int) (mCachedGroupPosInfo[groupPosition] & LOWER_31BIT_MASK);
+
+ mCachedGroupPosInfo[groupPosition] |= FLAG_EXPANDED;
+ mExpandedGroupCount += 1;
+
+ if (childCount > 0) {
+ mExpandedChildCount += childCount;
+ mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition);
+ // requires notifyItemRangeInserted()
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void moveGroupItem(int fromGroupPosition, int toGroupPosition) {
+ if (fromGroupPosition == toGroupPosition) {
+ return;
+ }
+
+ final long tmp1 = mCachedGroupPosInfo[fromGroupPosition];
+ final int tmp2 = mCachedGroupId[fromGroupPosition];
+
+ if (toGroupPosition < fromGroupPosition) {
+ // shift to backward
+ for (int i = fromGroupPosition; i > toGroupPosition; i--) {
+ mCachedGroupPosInfo[i] = mCachedGroupPosInfo[i - 1];
+ mCachedGroupId[i] = mCachedGroupId[i - 1];
+ }
+ } else {
+ // shift to forward
+ for (int i = fromGroupPosition; i < toGroupPosition; i++) {
+ mCachedGroupPosInfo[i] = mCachedGroupPosInfo[i + 1];
+ mCachedGroupId[i] = mCachedGroupId[i + 1];
+ }
+ }
+
+ mCachedGroupPosInfo[toGroupPosition] = tmp1;
+ mCachedGroupId[toGroupPosition] = tmp2;
+
+ final int minPosition = Math.min(fromGroupPosition, toGroupPosition);
+
+ if (minPosition > 0) {
+ mEndOfCalculatedOffsetGroupPosition = minPosition - 1;
+ } else {
+ mEndOfCalculatedOffsetGroupPosition = RecyclerView.NO_POSITION;
+ }
+ }
+
+ public void moveChildItem(int fromGroupPosition, int fromChildPosition, int toGroupPosition, int toChildPosition) {
+ if (fromGroupPosition == toGroupPosition) {
+ return;
+ }
+
+ final int fromChildCount = (int) (mCachedGroupPosInfo[fromGroupPosition] & LOWER_31BIT_MASK);
+ final int toChildCount = (int) (mCachedGroupPosInfo[toGroupPosition] & LOWER_31BIT_MASK);
+
+ if (fromChildCount == 0) {
+ throw new IllegalStateException("moveChildItem(" +
+ "fromGroupPosition = " + fromGroupPosition +
+ ", fromChildPosition = " + fromChildPosition +
+ ", toGroupPosition = " + toGroupPosition +
+ ", toChildPosition = " + toChildPosition + ") --- may be a bug.");
+ }
+
+ mCachedGroupPosInfo[fromGroupPosition] = (mCachedGroupPosInfo[fromGroupPosition] & (UPPER_32BIT_MASK | FLAG_EXPANDED)) | (fromChildCount - 1);
+ mCachedGroupPosInfo[toGroupPosition] = (mCachedGroupPosInfo[toGroupPosition] & (UPPER_32BIT_MASK | FLAG_EXPANDED)) | (toChildCount + 1);
+
+ if ((mCachedGroupPosInfo[fromGroupPosition] & FLAG_EXPANDED) != 0) {
+ mExpandedChildCount -= 1;
+ }
+ if ((mCachedGroupPosInfo[toGroupPosition] & FLAG_EXPANDED) != 0) {
+ mExpandedChildCount += 1;
+ }
+
+ final int minPosition = Math.min(fromGroupPosition, toGroupPosition);
+
+ if (minPosition > 0) {
+ mEndOfCalculatedOffsetGroupPosition = minPosition - 1;
+ } else {
+ mEndOfCalculatedOffsetGroupPosition = RecyclerView.NO_POSITION;
+ }
+ }
+
+ public long getExpandablePosition(int flatPosition) {
+ if (flatPosition == RecyclerView.NO_POSITION) {
+ return ExpandableAdapterHelper.NO_EXPANDABLE_POSITION;
+ }
+
+ final int groupCount = mGroupCount;
+
+ // final int startIndex = 0;
+ final int startIndex = binarySearchGroupPositionByFlatPosition(mCachedGroupPosInfo, mEndOfCalculatedOffsetGroupPosition, flatPosition);
+ long expandablePosition = ExpandableAdapterHelper.NO_EXPANDABLE_POSITION;
+ int endOfCalculatedOffsetGroupPosition = mEndOfCalculatedOffsetGroupPosition;
+ int offset = (startIndex == 0) ? 0 : (int) (mCachedGroupPosInfo[startIndex] >>> 32);
+
+ for (int i = startIndex; i < groupCount; i++) {
+ final long t = mCachedGroupPosInfo[i];
+
+ // update offset info
+ mCachedGroupPosInfo[i] = (((long) offset << 32) | (t & LOWER_32BIT_MASK));
+ endOfCalculatedOffsetGroupPosition = i;
+
+ if (offset >= flatPosition) {
+ // found (group item)
+ expandablePosition = ExpandableAdapterHelper.getPackedPositionForGroup(i);
+ break;
+ } else {
+ offset += 1;
+ }
+
+ if ((t & FLAG_EXPANDED) != 0) {
+ final int childCount = (int) (t & LOWER_31BIT_MASK);
+
+ if ((childCount > 0) && (offset + childCount - 1) >= flatPosition) {
+ // found (child item)
+ expandablePosition = ExpandableAdapterHelper.getPackedPositionForChild(i, (flatPosition - offset));
+ break;
+ } else {
+ offset += childCount;
+ }
+ }
+ }
+
+ mEndOfCalculatedOffsetGroupPosition = Math.max(mEndOfCalculatedOffsetGroupPosition, endOfCalculatedOffsetGroupPosition);
+
+ return expandablePosition;
+ }
+
+ public int getFlatPosition(long packedPosition) {
+ if (packedPosition == ExpandableAdapterHelper.NO_EXPANDABLE_POSITION) {
+ return RecyclerView.NO_POSITION;
+ }
+
+ final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(packedPosition);
+ final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(packedPosition);
+ final int groupCount = mGroupCount;
+
+ if (!(groupPosition >= 0 && groupPosition < groupCount)) {
+ return RecyclerView.NO_POSITION;
+ }
+
+ if (childPosition != RecyclerView.NO_POSITION) {
+ if (!isGroupExpanded(groupPosition)) {
+ return RecyclerView.NO_POSITION;
+ }
+ }
+
+ // final int startIndex = 0;
+ final int startIndex = Math.min(groupPosition, mEndOfCalculatedOffsetGroupPosition);
+ int endOfCalculatedOffsetGroupPosition = mEndOfCalculatedOffsetGroupPosition;
+ int offset = (int) (mCachedGroupPosInfo[startIndex] >>> 32);
+ int flatPosition = RecyclerView.NO_POSITION;
+
+ for (int i = startIndex; i < groupCount; i++) {
+ final long t = mCachedGroupPosInfo[i];
+
+ // update offset info
+ mCachedGroupPosInfo[i] = (((long) offset << 32) | (t & LOWER_32BIT_MASK));
+ endOfCalculatedOffsetGroupPosition = i;
+
+ final int childCount = (int) (t & LOWER_31BIT_MASK);
+
+ if (i == groupPosition) {
+ if (childPosition == RecyclerView.NO_POSITION) {
+ flatPosition = offset;
+ } else if (childPosition < childCount) {
+ flatPosition = (offset + 1) + childPosition;
+ }
+ break;
+ } else {
+ offset += 1;
+
+ if ((t & FLAG_EXPANDED) != 0) {
+ offset += childCount;
+ }
+ }
+ }
+
+ mEndOfCalculatedOffsetGroupPosition = Math.max(mEndOfCalculatedOffsetGroupPosition, endOfCalculatedOffsetGroupPosition);
+
+ return flatPosition;
+ }
+
+
+ private static int binarySearchGroupPositionByFlatPosition(long[] array, int endArrayPosition, int flatPosition) {
+ if (endArrayPosition <= 0) {
+ return 0;
+ }
+
+ final int v1 = (int) (array[0] >>> 32);
+ final int v2 = (int) (array[endArrayPosition] >>> 32);
+
+ if (flatPosition <= v1) {
+ return 0;
+ } else if (flatPosition >= v2) {
+ return endArrayPosition;
+ }
+
+ int lastS = 0;
+ int s = 0;
+ int e = endArrayPosition;
+
+ while (s < e) {
+ final int mid = (s + e) >>> 1;
+ final int v = (int) (array[mid] >>> 32);
+
+ if (v < flatPosition) {
+ lastS = s;
+ s = mid + 1;
+ } else {
+ e = mid;
+ }
+ }
+
+ return lastS;
+ }
+
+ public void removeGroupItem(int groupPosition) {
+ final long t = mCachedGroupPosInfo[groupPosition];
+
+ if ((t & FLAG_EXPANDED) != 0) {
+ mExpandedChildCount -= (int) (t & LOWER_31BIT_MASK);
+ mExpandedGroupCount -= 1;
+ }
+
+ mGroupCount -= 1;
+
+ // shift to forward
+ for (int i = groupPosition; i < mGroupCount; i++) {
+ mCachedGroupPosInfo[i] = mCachedGroupPosInfo[i + 1];
+ mCachedGroupId[i] = mCachedGroupId[i + 1];
+ }
+ }
+
+ public void removeChildItem(int groupPosition, int childPosition) {
+ final long t = mCachedGroupPosInfo[groupPosition];
+
+ if ((t & FLAG_EXPANDED) != 0) {
+ mExpandedChildCount -= 1;
+ }
+
+ mCachedGroupPosInfo[groupPosition] = (t & (UPPER_32BIT_MASK | FLAG_EXPANDED)) | ((t & LOWER_31BIT_MASK) - 1);
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableRecyclerViewWrapperAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableRecyclerViewWrapperAdapter.java
new file mode 100644
index 00000000000..0d8a3e9b42f
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableRecyclerViewWrapperAdapter.java
@@ -0,0 +1,658 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.expandable;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.ViewGroup;
+
+import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemAdapter;
+import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemViewHolder;
+import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange;
+import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager;
+import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager;
+import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter;
+import com.h6ah4i.android.widget.advrecyclerview.utils.BaseWrapperAdapter;
+import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils;
+
+class ExpandableRecyclerViewWrapperAdapter
+ extends BaseWrapperAdapter
+ implements DraggableItemAdapter,
+ SwipeableItemAdapter {
+ private static final String TAG = "ARVExpandableWrapper";
+
+ private static final int VIEW_TYPE_FLAG_IS_GROUP = ExpandableAdapterHelper.VIEW_TYPE_FLAG_IS_GROUP;
+
+ private static final int STATE_FLAG_INITIAL_VALUE = -1;
+
+ private ExpandableItemAdapter mExpandableItemAdapter;
+ private RecyclerViewExpandableItemManager mExpandableListManager;
+ private ExpandablePositionTranslator mPositionTranslator;
+ private int mDraggingItemGroupPosition = RecyclerView.NO_POSITION;
+
+ public ExpandableRecyclerViewWrapperAdapter(RecyclerViewExpandableItemManager manager, RecyclerView.Adapter adapter, int[] expandedItemsSavedState) {
+ super(adapter);
+
+ mExpandableItemAdapter = getExpandableItemAdapter(adapter);
+ if (mExpandableItemAdapter == null) {
+ throw new IllegalArgumentException("adapter does not implement RecyclerViewExpandableListManager");
+ }
+
+ if (manager == null) {
+ throw new IllegalArgumentException("manager cannot be null");
+ }
+
+ mExpandableListManager = manager;
+
+ mPositionTranslator = new ExpandablePositionTranslator();
+ mPositionTranslator.build(mExpandableItemAdapter);
+
+ if (expandedItemsSavedState != null) {
+ mPositionTranslator.restoreExpandedGroupItems(expandedItemsSavedState);
+ }
+ }
+
+ @Override
+ protected void onRelease() {
+ super.onRelease();
+
+ mExpandableItemAdapter = null;
+ mExpandableListManager = null;
+ }
+
+ @Override
+ public int getItemCount() {
+ return mPositionTranslator.getItemCount();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (mExpandableItemAdapter == null) {
+ return RecyclerView.NO_ID;
+ }
+
+ final long expandablePosition = mPositionTranslator.getExpandablePosition(position);
+ final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition);
+ final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition);
+
+ if (childPosition == RecyclerView.NO_POSITION) {
+ final long groupId = mExpandableItemAdapter.getGroupId(groupPosition);
+ return ExpandableAdapterHelper.getCombinedGroupId(groupId);
+ } else {
+ final long groupId = mExpandableItemAdapter.getGroupId(groupPosition);
+ final long childId = mExpandableItemAdapter.getChildId(groupPosition, childPosition);
+ return ExpandableAdapterHelper.getCombinedChildId(groupId, childId);
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (mExpandableItemAdapter == null) {
+ return 0;
+ }
+
+ final long expandablePosition = mPositionTranslator.getExpandablePosition(position);
+ final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition);
+ final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition);
+
+ final int type;
+
+ if (childPosition == RecyclerView.NO_POSITION) {
+ type = mExpandableItemAdapter.getGroupItemViewType(groupPosition);
+ } else {
+ type = mExpandableItemAdapter.getChildItemViewType(groupPosition, childPosition);
+ }
+
+ if ((type & VIEW_TYPE_FLAG_IS_GROUP) != 0) {
+ throw new IllegalStateException("Illegal view type (type = " + Integer.toHexString(type) + ")");
+ }
+
+ return (childPosition == RecyclerView.NO_POSITION) ? (type | VIEW_TYPE_FLAG_IS_GROUP) : (type);
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ if (mExpandableItemAdapter == null) {
+ return null;
+ }
+
+ final int maskedViewType = (viewType & (~VIEW_TYPE_FLAG_IS_GROUP));
+
+ final RecyclerView.ViewHolder holder;
+
+ if ((viewType & VIEW_TYPE_FLAG_IS_GROUP) != 0) {
+ holder = mExpandableItemAdapter.onCreateGroupViewHolder(parent, maskedViewType);
+ } else {
+ holder = mExpandableItemAdapter.onCreateChildViewHolder(parent, maskedViewType);
+ }
+
+ if (holder instanceof ExpandableItemViewHolder) {
+ ((ExpandableItemViewHolder) holder).setExpandStateFlags(STATE_FLAG_INITIAL_VALUE);
+ }
+
+ return holder;
+
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ if (mExpandableItemAdapter == null) {
+ return;
+ }
+
+ final long expandablePosition = mPositionTranslator.getExpandablePosition(position);
+ final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition);
+ final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition);
+ final int viewType = (holder.getItemViewType() & (~VIEW_TYPE_FLAG_IS_GROUP));
+
+ // update flags
+ int flags = 0;
+
+ if (childPosition == RecyclerView.NO_POSITION) {
+ flags |= RecyclerViewExpandableItemManager.STATE_FLAG_IS_GROUP;
+ } else {
+ flags |= RecyclerViewExpandableItemManager.STATE_FLAG_IS_CHILD;
+ }
+
+ if (mPositionTranslator.isGroupExpanded(groupPosition)) {
+ flags |= RecyclerViewExpandableItemManager.STATE_FLAG_IS_EXPANDED;
+ }
+
+ safeUpdateExpandStateFlags(holder, flags);
+
+ correctItemDragStateFlags(holder, groupPosition);
+
+ if (childPosition == RecyclerView.NO_POSITION) {
+ mExpandableItemAdapter.onBindGroupViewHolder(holder, groupPosition, viewType);
+ } else {
+ mExpandableItemAdapter.onBindChildViewHolder(holder, groupPosition, childPosition, viewType);
+ }
+ }
+
+ private void rebuildPositionTranslator() {
+ if (mPositionTranslator != null) {
+ int [] savedState = mPositionTranslator.getSavedStateArray();
+ mPositionTranslator.build(mExpandableItemAdapter);
+ mPositionTranslator.restoreExpandedGroupItems(savedState);
+ }
+ }
+
+ @Override
+ protected void onHandleWrappedAdapterChanged() {
+ rebuildPositionTranslator();
+ super.onHandleWrappedAdapterChanged();
+ }
+
+ @Override
+ protected void onHandleWrappedAdapterItemRangeChanged(int positionStart, int itemCount) {
+ super.onHandleWrappedAdapterItemRangeChanged(positionStart, itemCount);
+ }
+
+ @Override
+ protected void onHandleWrappedAdapterItemRangeInserted(int positionStart, int itemCount) {
+ rebuildPositionTranslator();
+ super.onHandleWrappedAdapterItemRangeInserted(positionStart, itemCount);
+ }
+
+ @Override
+ protected void onHandleWrappedAdapterItemRangeRemoved(int positionStart, int itemCount) {
+ if (itemCount == 1) {
+ final long expandablePosition = mPositionTranslator.getExpandablePosition(positionStart);
+ final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition);
+ final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition);
+
+ if (childPosition == RecyclerView.NO_POSITION) {
+ mPositionTranslator.removeGroupItem(groupPosition);
+ } else {
+ mPositionTranslator.removeChildItem(groupPosition, childPosition);
+ }
+ } else {
+ rebuildPositionTranslator();
+ }
+
+ super.onHandleWrappedAdapterItemRangeRemoved(positionStart, itemCount);
+ }
+
+ @Override
+ protected void onHandleWrappedAdapterRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ rebuildPositionTranslator();
+ super.onHandleWrappedAdapterRangeMoved(fromPosition, toPosition, itemCount);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean onCheckCanStartDrag(RecyclerView.ViewHolder holder, int x, int y) {
+ if (!(mExpandableItemAdapter instanceof ExpandableDraggableItemAdapter)) {
+ return false;
+ }
+
+ final ExpandableDraggableItemAdapter adapter = (ExpandableDraggableItemAdapter) mExpandableItemAdapter;
+
+ final int flatPosition = holder.getPosition();
+ final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition);
+ final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition);
+ final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition);
+
+ boolean canStart;
+ if (childPosition == RecyclerView.NO_POSITION) {
+ canStart = adapter.onCheckGroupCanStartDrag(holder, groupPosition, x, y);
+ } else {
+ canStart = adapter.onCheckChildCanStartDrag(holder, groupPosition, childPosition, x, y);
+ }
+
+ mDraggingItemGroupPosition = (canStart) ? groupPosition : RecyclerView.NO_POSITION;
+
+ return canStart;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public ItemDraggableRange onGetItemDraggableRange(RecyclerView.ViewHolder holder) {
+ if (!(mExpandableItemAdapter instanceof ExpandableDraggableItemAdapter)) {
+ return null;
+ }
+
+ if (mExpandableItemAdapter.getGroupCount() < 1) {
+ return null;
+ }
+
+ final ExpandableDraggableItemAdapter adapter = (ExpandableDraggableItemAdapter) mExpandableItemAdapter;
+
+ final int flatPosition = holder.getPosition();
+ final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition);
+ final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition);
+ final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition);
+
+ if (childPosition == RecyclerView.NO_POSITION) {
+ // group
+ final ItemDraggableRange groupRange = adapter.onGetGroupItemDraggableRange(holder, groupPosition);
+
+ if (groupRange == null) {
+ final int lastGroup = Math.max(0, mExpandableItemAdapter.getGroupCount() - 1);
+ final int start = 0;
+ final int end = Math.max(start, mPositionTranslator.getItemCount() - mPositionTranslator.getVisibleChildCount(lastGroup) - 1);
+
+ return new ItemDraggableRange(start, end);
+ } else {
+ final long startPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupRange.getStart());
+ final long endPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupRange.getEnd());
+ final int start = mPositionTranslator.getFlatPosition(startPackedGroupPosition);
+ int end = mPositionTranslator.getFlatPosition(endPackedGroupPosition);
+
+ if (groupRange.getEnd() > groupPosition) {
+ end += mPositionTranslator.getVisibleChildCount(groupRange.getEnd());
+ }
+
+ return new ItemDraggableRange(start, end);
+ }
+ } else {
+ // child
+ final ItemDraggableRange groupRange = adapter.onGetChildItemDraggableRange(holder, groupPosition, childPosition);
+
+ // NOTE:
+ // This method returns actual drag-sortable range, but the visual drag-sortable range would be different.
+ // Thus appending the STATE_FLAG_IS_IN_RANGE flag at correctItemDragStateFlags() to avoid visual corruption.
+ if (groupRange == null) {
+ final int start = 1; // 1 --- to avoid swapping with the first group item
+
+ return new ItemDraggableRange(start, Math.max(start, mPositionTranslator.getItemCount() - 1));
+ } else {
+ final long startPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupRange.getStart());
+ final long endPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupRange.getEnd());
+ final int end = mPositionTranslator.getFlatPosition(endPackedGroupPosition)
+ + mPositionTranslator.getVisibleChildCount(groupRange.getEnd());
+
+ int start = mPositionTranslator.getFlatPosition(startPackedGroupPosition) + 1;
+
+ start = Math.min(start, end);
+
+ return new ItemDraggableRange(start, end);
+ }
+ }
+ }
+
+ @Override
+ public void onMoveItem(int fromPosition, int toPosition) {
+ if (!(mExpandableItemAdapter instanceof ExpandableDraggableItemAdapter)) {
+ return;
+ }
+
+ mDraggingItemGroupPosition = RecyclerView.NO_POSITION;
+
+ if (fromPosition == toPosition) {
+ return;
+ }
+
+ final ExpandableDraggableItemAdapter adapter = (ExpandableDraggableItemAdapter) mExpandableItemAdapter;
+
+ final long expandableFromPosition = mPositionTranslator.getExpandablePosition(fromPosition);
+ final int fromGroupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandableFromPosition);
+ final int fromChildPosition = ExpandableAdapterHelper.getPackedPositionChild(expandableFromPosition);
+
+ final long expandableToPosition = mPositionTranslator.getExpandablePosition(toPosition);
+ final int toGroupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandableToPosition);
+ final int toChildPosition = ExpandableAdapterHelper.getPackedPositionChild(expandableToPosition);
+
+ final boolean fromIsGroup = (fromChildPosition == RecyclerView.NO_POSITION);
+ final boolean toIsGroup = (toChildPosition == RecyclerView.NO_POSITION);
+
+ int actualToFlatPosition = fromPosition;
+
+ if (fromIsGroup && toIsGroup) {
+ adapter.onMoveGroupItem(fromGroupPosition, toGroupPosition);
+ mPositionTranslator.moveGroupItem(fromGroupPosition, toGroupPosition);
+ actualToFlatPosition = toPosition;
+ } else if (!fromIsGroup && !toIsGroup) {
+ int modToChildPosition;
+
+ // correct child position
+ if (fromGroupPosition == toGroupPosition) {
+ modToChildPosition = toChildPosition;
+ } else {
+ if (fromPosition < toPosition) {
+ modToChildPosition = toChildPosition + 1;
+ } else {
+ modToChildPosition = toChildPosition;
+ }
+ }
+
+ actualToFlatPosition = mPositionTranslator.getFlatPosition(
+ ExpandableAdapterHelper.getPackedPositionForChild(fromGroupPosition, modToChildPosition));
+
+ adapter.onMoveChildItem(fromGroupPosition, fromChildPosition, toGroupPosition, modToChildPosition);
+ mPositionTranslator.moveChildItem(fromGroupPosition, fromChildPosition, toGroupPosition, modToChildPosition);
+ } else if (!fromIsGroup /*&& toIsGroup NOTE: toIsGroup is always true here*/) {
+ int modToGroupPosition;
+ int modToChildPosition;
+
+ if (toPosition < fromPosition) {
+ if (toGroupPosition == 0) {
+ // insert at the top
+ modToGroupPosition = toGroupPosition;
+ modToChildPosition = 0;
+ } else {
+ // insert at the end
+ modToGroupPosition = toGroupPosition - 1;
+ modToChildPosition = mPositionTranslator.getChildCount(modToGroupPosition);
+ }
+ } else {
+ if (mPositionTranslator.isGroupExpanded(toGroupPosition)) {
+ // insert at the top
+ modToGroupPosition = toGroupPosition;
+ modToChildPosition = 0;
+ } else {
+ // insert at the end
+ modToGroupPosition = toGroupPosition;
+ modToChildPosition = mPositionTranslator.getChildCount(modToGroupPosition);
+ }
+ }
+
+ if (fromGroupPosition == modToGroupPosition) {
+ final int lastIndex = Math.max(0, mPositionTranslator.getChildCount(modToGroupPosition) - 1);
+ modToChildPosition = Math.min(modToChildPosition, lastIndex);
+ }
+
+ if (!((fromGroupPosition == modToGroupPosition) && (fromChildPosition == modToChildPosition))) {
+ if (mPositionTranslator.isGroupExpanded(toGroupPosition)) {
+ actualToFlatPosition = toPosition;
+ } else {
+ actualToFlatPosition = RecyclerView.NO_POSITION;
+ }
+
+ adapter.onMoveChildItem(fromGroupPosition, fromChildPosition, modToGroupPosition, modToChildPosition);
+ mPositionTranslator.moveChildItem(fromGroupPosition, fromChildPosition, modToGroupPosition, modToChildPosition);
+ }
+ } else { // if (fromIsGroup && !toIsGroup)
+ if (fromGroupPosition != toGroupPosition) {
+ actualToFlatPosition = mPositionTranslator.getFlatPosition(ExpandableAdapterHelper.getPackedPositionForGroup(toGroupPosition));
+
+ adapter.onMoveGroupItem(fromGroupPosition, toGroupPosition);
+ mPositionTranslator.moveGroupItem(fromGroupPosition, toGroupPosition);
+ }
+ }
+
+ if (actualToFlatPosition != fromPosition) {
+ if (actualToFlatPosition != RecyclerView.NO_POSITION) {
+ notifyItemMoved(fromPosition, actualToFlatPosition);
+ } else {
+ notifyItemRemoved(fromPosition);
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public int onGetSwipeReactionType(RecyclerView.ViewHolder holder, int x, int y) {
+ if (!(mExpandableItemAdapter instanceof ExpandableSwipeableItemAdapter)) {
+ return RecyclerViewSwipeManager.REACTION_CAN_NOT_SWIPE_BOTH;
+ }
+
+ final ExpandableSwipeableItemAdapter adapter = (ExpandableSwipeableItemAdapter) mExpandableItemAdapter;
+
+ final long expandablePosition = mPositionTranslator.getExpandablePosition(holder.getPosition());
+ final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition);
+ final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition);
+
+ if (childPosition == RecyclerView.NO_POSITION) {
+ return adapter.onGetGroupItemSwipeReactionType(holder, groupPosition, x, y);
+ } else {
+ return adapter.onGetChildItemSwipeReactionType(holder, groupPosition, childPosition, x, y);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onSetSwipeBackground(RecyclerView.ViewHolder holder, int type) {
+ if (!(mExpandableItemAdapter instanceof ExpandableSwipeableItemAdapter)) {
+ return;
+ }
+
+ final ExpandableSwipeableItemAdapter adapter = (ExpandableSwipeableItemAdapter) mExpandableItemAdapter;
+
+ final long expandablePosition = mPositionTranslator.getExpandablePosition(holder.getPosition());
+ final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition);
+ final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition);
+
+ if (childPosition == RecyclerView.NO_POSITION) {
+ adapter.onSetGroupItemSwipeBackground(holder, groupPosition, type);
+ } else {
+ adapter.onSetChildItemSwipeBackground(holder, groupPosition, childPosition, type);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public int onSwipeItem(RecyclerView.ViewHolder holder, int result) {
+ if (!(mExpandableItemAdapter instanceof ExpandableSwipeableItemAdapter)) {
+ return RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT;
+ }
+
+ final ExpandableSwipeableItemAdapter adapter = (ExpandableSwipeableItemAdapter) mExpandableItemAdapter;
+
+ final long expandablePosition = mPositionTranslator.getExpandablePosition(holder.getPosition());
+ final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition);
+ final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition);
+
+ if (childPosition == RecyclerView.NO_POSITION) {
+ return adapter.onSwipeGroupItem(holder, groupPosition, result);
+ } else {
+ return adapter.onSwipeChildItem(holder, groupPosition, childPosition, result);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void onPerformAfterSwipeReaction(RecyclerView.ViewHolder holder, int result, int reaction) {
+ if (!(mExpandableItemAdapter instanceof ExpandableSwipeableItemAdapter)) {
+ return;
+ }
+
+ final ExpandableSwipeableItemAdapter adapter = (ExpandableSwipeableItemAdapter) mExpandableItemAdapter;
+
+ final long expandablePosition = mPositionTranslator.getExpandablePosition(holder.getPosition());
+ final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition);
+ final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition);
+
+ if (childPosition == RecyclerView.NO_POSITION) {
+ adapter.onPerformAfterSwipeGroupReaction(holder, groupPosition, result, reaction);
+ } else {
+ adapter.onPerformAfterSwipeChildReaction(holder, groupPosition, childPosition, result, reaction);
+ }
+ }
+ // NOTE: This method is called from RecyclerViewExpandableItemManager
+ /*package*/
+ @SuppressWarnings("unchecked")
+ boolean onTapItem(RecyclerView.ViewHolder holder, int x, int y) {
+ if (mExpandableItemAdapter == null) {
+ return false;
+ }
+
+ final int position = holder.getPosition();
+ final long expandablePosition = mPositionTranslator.getExpandablePosition(position);
+ final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition);
+ final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition);
+
+ if (childPosition != RecyclerView.NO_POSITION) {
+ return false;
+ }
+
+ final boolean expand = !(mPositionTranslator.isGroupExpanded(groupPosition));
+
+ boolean result = mExpandableItemAdapter.onCheckCanExpandOrCollapseGroup(holder, groupPosition, x, y, expand);
+
+ if (!result) {
+ return false;
+ }
+
+ if (expand) {
+ expandGroup(groupPosition);
+ } else {
+ collapseGroup(groupPosition);
+ }
+
+ return true;
+ }
+
+ /*package*/ boolean collapseGroup(int groupPosition) {
+ if (!mPositionTranslator.isGroupExpanded(groupPosition)) {
+ return false;
+ }
+
+ if (mPositionTranslator.collapseGroup(groupPosition)) {
+ final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition);
+ final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition);
+ final int childCount = mPositionTranslator.getChildCount(groupPosition);
+
+ notifyItemRangeRemoved(flatPosition + 1, childCount);
+ }
+
+
+ {
+ final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition);
+ final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition);
+
+ notifyItemChanged(flatPosition);
+ }
+
+ return true;
+ }
+
+ /*package*/ boolean expandGroup(int groupPosition) {
+ if (mPositionTranslator.isGroupExpanded(groupPosition)) {
+ return false;
+ }
+
+
+ if (mPositionTranslator.expandGroup(groupPosition)) {
+ final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition);
+ final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition);
+ final int childCount = mPositionTranslator.getChildCount(groupPosition);
+
+ notifyItemRangeInserted(flatPosition + 1, childCount);
+ }
+
+ {
+ final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition);
+ final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition);
+
+ notifyItemChanged(flatPosition);
+ }
+
+ return true;
+ }
+
+ /*package*/ boolean isGroupExpanded(int groupPosition) {
+ return mPositionTranslator.isGroupExpanded(groupPosition);
+ }
+
+ /*package*/ long getExpandablePosition(int flatPosition) {
+ return mPositionTranslator.getExpandablePosition(flatPosition);
+ }
+
+ /*package*/ int getFlatPosition(long packedPosition) {
+ return mPositionTranslator.getFlatPosition(packedPosition);
+ }
+
+ /*package*/ int[] getExpandedItemsSavedStateArray() {
+ if (mPositionTranslator != null) {
+ return mPositionTranslator.getSavedStateArray();
+ } else {
+ return null;
+ }
+ }
+
+ private static ExpandableItemAdapter getExpandableItemAdapter(RecyclerView.Adapter adapter) {
+ return WrapperAdapterUtils.findWrappedAdapter(adapter, ExpandableItemAdapter.class);
+ }
+
+ private static void safeUpdateExpandStateFlags(RecyclerView.ViewHolder holder, int flags) {
+ if (!(holder instanceof ExpandableItemViewHolder)) {
+ return;
+ }
+
+ final ExpandableItemViewHolder holder2 = (ExpandableItemViewHolder) holder;
+
+ final int curFlags = holder2.getExpandStateFlags();
+ final int mask = ~RecyclerViewExpandableItemManager.STATE_FLAG_IS_UPDATED;
+
+ // append UPDATED flag
+ if ((curFlags == STATE_FLAG_INITIAL_VALUE) || (((curFlags ^ flags) & mask) != 0)) {
+ flags |= RecyclerViewExpandableItemManager.STATE_FLAG_IS_UPDATED;
+ }
+
+ holder2.setExpandStateFlags(flags);
+ }
+
+ private void correctItemDragStateFlags(RecyclerView.ViewHolder holder, int groupPosition) {
+ if (!(holder instanceof DraggableItemViewHolder)) {
+ return;
+ }
+
+ final DraggableItemViewHolder holder2 = (DraggableItemViewHolder) holder;
+
+ final int flags = holder2.getDragStateFlags();
+
+ if (((flags & RecyclerViewDragDropManager.STATE_FLAG_DRAGGING) != 0) &&
+ ((flags & RecyclerViewDragDropManager.STATE_FLAG_IS_IN_RANGE) == 0)) {
+ if (mDraggingItemGroupPosition == groupPosition) {
+ holder2.setDragStateFlags(
+ flags | RecyclerViewDragDropManager.STATE_FLAG_IS_IN_RANGE |
+ RecyclerViewDragDropManager.STATE_FLAG_IS_UPDATED);
+ }
+ }
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableSwipeableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableSwipeableItemAdapter.java
new file mode 100644
index 00000000000..8f8a6c88a24
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableSwipeableItemAdapter.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.expandable;
+
+import android.support.v7.widget.RecyclerView;
+
+public interface ExpandableSwipeableItemAdapter {
+ /**
+ * Called when user is attempt to swipe the group item.
+ *
+ * @param holder The ViewHolder which is associated to item user is attempt to start swiping.
+ * @param groupPosition Group position.
+ * @param x Touched X position. Relative from the itemView's top-left.
+ * @param y Touched Y position. Relative from the itemView's top-left.
+
+ * @return Reaction type. Bitwise OR of these flags;
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_LEFT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_LEFT_WITH_RUBBER_BAND_EFFECT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_SWIPE_LEFT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_RIGHT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_SWIPE_RIGHT}
+ */
+ int onGetGroupItemSwipeReactionType(GVH holder, int groupPosition, int x, int y);
+
+ /**
+ * Called when user is attempt to swipe the child item.
+ *
+ * @param holder The ViewHolder which is associated to item user is attempt to start swiping.
+ * @param groupPosition Group position.
+ * @param childPosition Child position.
+ * @param x Touched X position. Relative from the itemView's top-left.
+ * @param y Touched Y position. Relative from the itemView's top-left.
+
+ * @return Reaction type. Bitwise OR of these flags;
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_LEFT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_LEFT_WITH_RUBBER_BAND_EFFECT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_SWIPE_LEFT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_RIGHT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#REACTION_CAN_SWIPE_RIGHT}
+ */
+ int onGetChildItemSwipeReactionType(CVH holder, int groupPosition, int childPosition, int x, int y);
+
+ /**
+ * Called when sets background of the swiping group item.
+ *
+ * @param holder The ViewHolder which is associated to the swiping item.
+ * @param groupPosition Group position.
+ * @param type Background type. One of the
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#DRAWABLE_SWIPE_NEUTRAL_BACKGROUND},
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#DRAWABLE_SWIPE_LEFT_BACKGROUND} or
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#DRAWABLE_SWIPE_RIGHT_BACKGROUND}.
+ */
+ void onSetGroupItemSwipeBackground(GVH holder, int groupPosition, int type);
+
+
+ /**
+ * Called when sets background of the swiping child item.
+ *
+ * @param holder The ViewHolder which is associated to the swiping item.
+ * @param groupPosition Group position.
+ * @param childPosition Child position.
+ * @param type Background type. One of the
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#DRAWABLE_SWIPE_NEUTRAL_BACKGROUND},
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#DRAWABLE_SWIPE_LEFT_BACKGROUND} or
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#DRAWABLE_SWIPE_RIGHT_BACKGROUND}.
+ */
+ void onSetChildItemSwipeBackground(CVH holder, int groupPosition, int childPosition, int type);
+
+ /**
+ * Called when group item is swiped.
+ *
+ * *Note that do not change data set and do not call notifyDataXXX() methods inside of this method.*
+ *
+ * @param holder The ViewHolder which is associated to the swiped item.
+ * @param groupPosition Group position.
+ * @param result The result code of user's swipe operation.
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_CANCELED},
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT}
+ *
+ * @return Reaction type of after swiping.
+ * One of the {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT},
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} or
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}.
+ */
+ int onSwipeGroupItem(GVH holder, int groupPosition, int result);
+
+ /**
+ * Called when child item is swiped.
+ *
+ * *Note that do not change data set and do not call notifyDataXXX() methods inside of this method.*
+ *
+ * @param holder The ViewHolder which is associated to the swiped item.
+ * @param groupPosition Group position.
+ * @param childPosition Child position.
+ * @param result The result code of user's swipe operation.
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_CANCELED},
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT}
+ *
+ * @return Reaction type of after swiping.
+ * One of the {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT},
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} or
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}.
+ */
+ int onSwipeChildItem(CVH holder, int groupPosition, int childPosition, int result);
+
+ /**
+ * Called after {@link #onSwipeGroupItem(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method.
+ *
+ * You can update data set and call notifyDataXXX() methods inside of this method.
+ *
+ * @param holder The ViewHolder which is associated to the swiped item.
+ * @param groupPosition Group position.
+ * @param result The result code of user's swipe operation.
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_CANCELED},
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT}
+ * @param reaction Reaction type. One of the {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT},
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} or
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}.
+ */
+ void onPerformAfterSwipeGroupReaction(GVH holder, int groupPosition, int result, int reaction);
+
+
+ /**
+ * Called after {@link #onSwipeChildItem(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method.
+ *
+ * You can update data set and call notifyDataXXX() methods inside of this method.
+ *
+ * @param holder The ViewHolder which is associated to the swiped item.
+ * @param groupPosition Group position.
+ * @param childPosition Child position.
+ * @param result The result code of user's swipe operation.
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_CANCELED},
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT}
+ * @param reaction Reaction type. One of the {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT},
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} or
+ * {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}.
+ */
+ void onPerformAfterSwipeChildReaction(CVH holder, int groupPosition, int childPosition, int result, int reaction);
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/RecyclerViewExpandableItemManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/RecyclerViewExpandableItemManager.java
new file mode 100644
index 00000000000..206bf9d81e6
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/RecyclerViewExpandableItemManager.java
@@ -0,0 +1,472 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.expandable;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import com.h6ah4i.android.widget.advrecyclerview.utils.CustomRecyclerViewUtils;
+
+/**
+ * Provides item swipe operation for {@link android.support.v7.widget.RecyclerView}
+ */
+public class RecyclerViewExpandableItemManager {
+ private static final String TAG = "ARVExpandableItemMgr";
+
+ /**
+ * Packed position version of {@link android.support.v7.widget.RecyclerView#NO_POSITION}
+ */
+ public static final long NO_EXPANDABLE_POSITION = ExpandableAdapterHelper.NO_EXPANDABLE_POSITION;
+
+
+ /**
+ * State flag for the {@link ExpandableItemViewHolder#setExpandStateFlags(int)} and {@link ExpandableItemViewHolder#getExpandStateFlags()} methods.
+ * Indicates that this ViewHolder is associated to group item.
+ */
+ @SuppressWarnings("PointlessBitwiseExpression")
+ public static final int STATE_FLAG_IS_GROUP = (1 << 0);
+
+ /**
+ * State flag for the {@link ExpandableItemViewHolder#setExpandStateFlags(int)} and {@link ExpandableItemViewHolder#getExpandStateFlags()} methods.
+ * Indicates that this ViewHolder is associated to child item.
+ */
+ public static final int STATE_FLAG_IS_CHILD = (1 << 1);
+
+ /**
+ * State flag for the {@link ExpandableItemViewHolder#setExpandStateFlags(int)} and {@link ExpandableItemViewHolder#getExpandStateFlags()} methods.
+ * Indicates that this is a group item.
+ */
+ public static final int STATE_FLAG_IS_EXPANDED = (1 << 2);
+
+ /**
+ * State flag for the {@link ExpandableItemViewHolder#setExpandStateFlags(int)} and {@link ExpandableItemViewHolder#getExpandStateFlags()} methods.
+ * If this flag is set, some other flags are changed and require to apply.
+ */
+ public static final int STATE_FLAG_IS_UPDATED = (1 << 31);
+
+ // ---
+
+ private SavedState mSavedState;
+
+ private RecyclerView mRecyclerView;
+ private ExpandableRecyclerViewWrapperAdapter mAdapter;
+ private RecyclerView.OnItemTouchListener mInternalUseOnItemTouchListener;
+
+ private long mTouchedItemId = RecyclerView.NO_ID;
+ private int mTouchSlop;
+ private int mInitialTouchX;
+ private int mInitialTouchY;
+
+ /**
+ * Constructor.
+ *
+ * @param savedState The saved state object which is obtained from the {@link #getSavedState()} method.
+ */
+ public RecyclerViewExpandableItemManager(Parcelable savedState) {
+ mInternalUseOnItemTouchListener = new RecyclerView.OnItemTouchListener() {
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ return RecyclerViewExpandableItemManager.this.onInterceptTouchEvent(rv, e);
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ }
+ };
+
+ if (savedState instanceof SavedState) {
+ mSavedState = (SavedState) savedState;
+ }
+ }
+
+ /**
+ * Indicates this manager instance has released or not.
+ *
+ * @return True if this manager instance has released
+ */
+ public boolean isReleased() {
+ return (mInternalUseOnItemTouchListener == null);
+ }
+
+ /**
+ * Attaches {@link android.support.v7.widget.RecyclerView} instance.
+ *
+ * Before calling this method, the target {@link android.support.v7.widget.RecyclerView} must set
+ * the wrapped adapter instance which is returned by the
+ * {@link #createWrappedAdapter(android.support.v7.widget.RecyclerView.Adapter)} method.
+ *
+ * @param rv The {@link android.support.v7.widget.RecyclerView} instance
+ */
+ public void attachRecyclerView(RecyclerView rv) {
+ if (rv == null) {
+ throw new IllegalArgumentException("RecyclerView cannot be null");
+ }
+
+ if (isReleased()) {
+ throw new IllegalStateException("Accessing released object");
+ }
+
+ if (mRecyclerView != null) {
+ throw new IllegalStateException("RecyclerView instance has already been set");
+ }
+
+ mRecyclerView = rv;
+ mRecyclerView.addOnItemTouchListener(mInternalUseOnItemTouchListener);
+ mTouchSlop = ViewConfiguration.get(mRecyclerView.getContext()).getScaledTouchSlop();
+ }
+
+ /**
+ * Detach the {@link android.support.v7.widget.RecyclerView} instance and release internal field references.
+ *
+ * This method should be called in order to avoid memory leaks.
+ */
+ public void release() {
+ if (mRecyclerView != null && mInternalUseOnItemTouchListener != null) {
+ mRecyclerView.removeOnItemTouchListener(mInternalUseOnItemTouchListener);
+ }
+ mInternalUseOnItemTouchListener = null;
+ mRecyclerView = null;
+ mSavedState = null;
+ }
+
+ /**
+ * Create wrapped adapter.
+ *
+ * @param adapter The target adapter.
+ *
+ * @return Wrapped adapter which is associated to this {@link RecyclerViewExpandableItemManager} instance.
+ */
+ @SuppressWarnings("unchecked")
+ public RecyclerView.Adapter createWrappedAdapter(RecyclerView.Adapter adapter) {
+ if (mAdapter != null) {
+ throw new IllegalStateException("already have a wrapped adapter");
+ }
+
+ int [] adapterSavedState = (mSavedState != null) ? mSavedState.adapterSavedState : null;
+ mSavedState = null;
+
+ mAdapter = new ExpandableRecyclerViewWrapperAdapter(this, adapter, adapterSavedState);
+
+ return mAdapter;
+ }
+
+ /**
+ * Gets saved state object in order to restore the internal state.
+ *
+ * Call this method in Activity/Fragment's onSavedInstance() and save to the bundle.
+ *
+ * @return The Parcelable object which stores information need to restore the internal states.
+ */
+ public Parcelable getSavedState() {
+ int [] adapterSavedState = null;
+
+ if (mAdapter != null) {
+ adapterSavedState = mAdapter.getExpandedItemsSavedStateArray();
+ }
+
+ return new SavedState(adapterSavedState);
+ }
+
+ /*package*/ boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ if (mAdapter == null) {
+ return false;
+ }
+
+ final int action = MotionEventCompat.getActionMasked(e);
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ handleActionDown(rv, e);
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ if (handleActionUpOrCancel(rv, e)) {
+ return true;
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ private void handleActionDown(RecyclerView rv, MotionEvent e) {
+ final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithTranslation(rv, e.getX(), e.getY());
+
+ mInitialTouchX = (int) (e.getX() + 0.5f);
+ mInitialTouchY = (int) (e.getY() + 0.5f);
+
+ if (holder != null) {
+ mTouchedItemId = holder.getItemId();
+ } else {
+ mTouchedItemId = RecyclerView.NO_ID;
+ }
+ }
+
+ private boolean handleActionUpOrCancel(RecyclerView rv, MotionEvent e) {
+ final long touchedItemId = mTouchedItemId;
+ final int initialTouchX = mInitialTouchX;
+ final int initialTouchY = mInitialTouchY;
+
+ mTouchedItemId = RecyclerView.NO_ID;
+ mInitialTouchX = 0;
+ mInitialTouchY = 0;
+
+ if (!((touchedItemId != RecyclerView.NO_ID) && (MotionEventCompat.getActionMasked(e) == MotionEvent.ACTION_UP))) {
+ return false;
+ }
+
+ final int touchX = (int) (e.getX() + 0.5f);
+ final int touchY = (int) (e.getY() + 0.5f);
+
+ final int diffX = touchX - initialTouchX;
+ final int diffY = touchY - initialTouchY;
+
+ if (!((Math.abs(diffX) < mTouchSlop) && (Math.abs(diffY) < mTouchSlop))) {
+ return false;
+ }
+
+ final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithTranslation(rv, e.getX(), e.getY());
+
+ if (!((holder != null) && (holder.getItemId() == touchedItemId))) {
+ return false;
+ }
+
+ final View view = holder.itemView;
+ final int translateX = (int) (ViewCompat.getTranslationX(view) + 0.5f);
+ final int translateY = (int) (ViewCompat.getTranslationY(view) + 0.5f);
+ final int viewX = touchX - (view.getLeft() + translateX);
+ final int viewY = touchY - (view.getTop() + translateY);
+
+ return mAdapter.onTapItem(holder, viewX, viewY);
+ }
+
+ /**
+ * Expand a group.
+ *
+ * @param groupPosition The group position to be expanded
+ *
+ * @return True if the group was expanded,false otherwise (If the group was already expanded, this will return false)
+ */
+ public boolean expandGroup(int groupPosition) {
+ return (mAdapter != null) && mAdapter.expandGroup(groupPosition);
+ }
+
+ /**
+ * Collapse a group.
+ *
+ * @param groupPosition The group position to be collapsed
+ *
+ * @return True if the group was collapsed,false otherwise (If the group was already collapsed, this will return false)
+ */
+ public boolean collapseGroup(int groupPosition) {
+ return (mAdapter != null) && mAdapter.collapseGroup(groupPosition);
+ }
+
+ /**
+ * Converts a flat position (the raw position of an item in the list) to a group and/or child position
+ * (represented in a packed position). Use {@link #getPackedPositionChild(long)}, {@link #getPackedPositionGroup(long)} to unpack.
+ *
+ * @param flatPosition The flat position to be converted
+ *
+ * @return The group and/or child position for the given flat position in packed position representation.
+ */
+ public long getExpandablePosition(int flatPosition) {
+ if (mAdapter == null) {
+ return ExpandableAdapterHelper.NO_EXPANDABLE_POSITION;
+ }
+ return mAdapter.getExpandablePosition(flatPosition);
+ }
+
+ /**
+ * Converts a group and/or child position to a flat position.
+ *
+ * @param packedPosition The group and/or child position to be converted in packed position representation.
+ *
+ * @return The group and/or child position for the given flat position in packed position representation.
+ */
+ public int getFlatPosition(long packedPosition) {
+ if (mAdapter == null) {
+ return RecyclerView.NO_POSITION;
+ }
+ return mAdapter.getFlatPosition(packedPosition);
+ }
+
+ /**
+ * Gets the child position from a packed position.
+ * To get the group that this child belongs to, use {@link #getPackedPositionGroup(long)}.
+ * See {@link #getPackedPositionForChild(int, int)}.
+ *
+ * @param packedPosition The packed position from which the child position will be returned.
+ *
+ * @return The child position portion of the packed position. If this does not contain a child, returns {@link android.support.v7.widget.RecyclerView#NO_POSITION}.
+ */
+ public static int getPackedPositionChild(long packedPosition) {
+ return ExpandableAdapterHelper.getPackedPositionChild(packedPosition);
+ }
+
+ /**
+ * Returns the packed position representation of a child position.
+ *
+ * In general, a packed position should be used in situations where the position given to/returned from
+ * {@link RecyclerViewExpandableItemManager} method can eirther be a child or group.
+ * The two positions are packed into a single long which can be unpacked using {@link #getPackedPositionChild(long)} and
+ * {@link #getPackedPositionGroup(long)}.
+ *
+ * @param groupPosition The child's parent group's position
+ * @param childPosition The child position within the group
+ *
+ * @return The packed position representation of the child (and parent group).
+ */
+ public static long getPackedPositionForChild(int groupPosition, int childPosition) {
+ return ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, childPosition);
+ }
+
+ /**
+ * Returns the packed position representation of a group's position. See {@link #getPackedPositionForChild(int, int)}.
+ *
+ * @param groupPosition The child's parent group's position.
+ *
+ * @return The packed position representation of the group.
+ */
+ public static long getPackedPositionForGroup(int groupPosition) {
+ return ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition);
+ }
+
+ /**
+ * Gets the group position from a packed position. See {@link #getPackedPositionForChild(int, int)}.
+ *
+ * @param packedPosition The packed position from which the group position will be returned.
+ *
+ * @return THe group position of the packed position. If this does not contain a group, returns {@link android.support.v7.widget.RecyclerView#NO_POSITION}.
+ */
+ public static int getPackedPositionGroup(long packedPosition) {
+ return ExpandableAdapterHelper.getPackedPositionGroup(packedPosition);
+ }
+
+ /**
+ * Whether the given group is currently expanded.
+ *
+ * @param groupPosition The group to check
+ *
+ * @return Whether the group is currently expanded
+ */
+ public boolean isGroupExpanded(int groupPosition) {
+ return (mAdapter != null) && mAdapter.isGroupExpanded(groupPosition);
+ }
+
+ /**
+ * Gets combined ID for child item.
+ *
+ * bit 0-31: Lower 32 bits of the childId
+ * bit 32-62: Lower 31 bits of the groupId
+ * bit 63: reserved
+ *
+ * @param groupId The ID of the group that contains the child.
+ * @param childId The ID of the child.
+ *
+ * @return The unique ID of the child across all groups and children in the list
+ */
+ public static long getCombinedChildId(long groupId, long childId) {
+ return ExpandableAdapterHelper.getCombinedChildId(groupId, childId);
+ }
+
+ /**
+ * Gets combined ID for child item.
+ *
+ * bit 0-31: all bits are set to 1
+ * bit 32-62: Lower 31 bits of the groupId
+ * bit 63: reserved
+ *
+ * @param groupId The ID of the group that contains the child.
+ *
+ * @return The unique ID of the child across all groups and children in the list
+ */
+ public static long getCombinedGroupId(long groupId) {
+ return ExpandableAdapterHelper.getCombinedGroupId(groupId);
+ }
+
+ /**
+ * Checks whether the passed view type is a group's one.
+ *
+ * @param rawViewType raw view type value (return value of {@link android.support.v7.widget.RecyclerView.ViewHolder#getItemViewType()})
+ *
+ * @return True for the a group view type, otherwise false
+ */
+ public static boolean isGroupViewType(int rawViewType) {
+ return ExpandableAdapterHelper.isGroupViewType(rawViewType);
+ }
+
+ /**
+ * Gets group view type from a raw view type.
+ *
+ * @param rawViewType raw view type value (return value of {@link android.support.v7.widget.RecyclerView.ViewHolder#getItemViewType()})
+ *
+ * @return Group view type for the given raw view type.
+ */
+ public static int getGroupViewType(int rawViewType) {
+ return ExpandableAdapterHelper.getGroupViewType(rawViewType);
+ }
+
+ /**
+ * Gets child view type from a raw view type.
+ *
+ * @param rawViewType raw view type value (return value of {@link android.support.v7.widget.RecyclerView.ViewHolder#getItemViewType()})
+ *
+ * @return Child view type for the given raw view type.
+ */
+ public static int getChildViewType(int rawViewType) {
+ return ExpandableAdapterHelper.getChildViewType(rawViewType);
+ }
+
+ public static class SavedState implements Parcelable {
+ final int [] adapterSavedState;
+
+ public SavedState(int[] adapterSavedState) {
+ this.adapterSavedState = adapterSavedState;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeIntArray(this.adapterSavedState);
+ }
+
+ private SavedState(Parcel in) {
+ this.adapterSavedState = in.createIntArray();
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ public SavedState createFromParcel(Parcel source) {
+ return new SavedState(source);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/ItemSlidingAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/ItemSlidingAnimator.java
new file mode 100644
index 00000000000..925a006146c
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/ItemSlidingAnimator.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.swipeable;
+
+import android.annotation.SuppressLint;
+import android.graphics.Rect;
+import android.os.Build;
+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.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ItemSlidingAnimator {
+ private static final String TAG = "ItemSlidingAnimator";
+
+ private Interpolator mSlideToDefaultPositionAnimationInterpolator = new AccelerateDecelerateInterpolator();
+ private Interpolator mSlideToOutsideOfWindowAnimationInterpolator = new AccelerateInterpolator(0.8f);
+ private List mActive;
+ private int[] mTmpLocation = new int[2];
+ private Rect mTmpRect = new Rect();
+ private int mImmediatelySetTranslationThreshold;
+
+ public ItemSlidingAnimator() {
+ mActive = new ArrayList<>();
+ }
+
+ public void slideToDefaultPosition(RecyclerView.ViewHolder holder, boolean shouldAnimate, long duration) {
+ slideToSpecifiedPositionInternal(holder, 0, shouldAnimate, duration);
+ }
+
+ public void slideToOutsideOfWindow(RecyclerView.ViewHolder holder, boolean toLeft, boolean shouldAnimate, long duration) {
+ slideToOutsideOfWindowInternal(holder, toLeft, shouldAnimate, duration);
+ }
+
+ public void slideToSpecifiedPosition(RecyclerView.ViewHolder holder, float position) {
+ slideToSpecifiedPositionInternal(holder, position, false, 0);
+ }
+
+ private boolean slideToSpecifiedPositionInternal(RecyclerView.ViewHolder holder, float position, boolean shouldAnimate, long duration) {
+ final int translationX;
+
+ duration = (shouldAnimate) ? duration : 0;
+
+ if (position != 0.0f) {
+ final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView();
+
+ translationX = (int) (containerView.getWidth() * position + 0.5f);
+ } else {
+ translationX = 0;
+ }
+
+ return animateSlideInternalCompat(holder, translationX, duration, mSlideToDefaultPositionAnimationInterpolator);
+ }
+
+ private boolean slideToOutsideOfWindowInternal(RecyclerView.ViewHolder holder, boolean toLeft, boolean shouldAnimate, long duration) {
+ if (!(holder instanceof SwipeableItemViewHolder)) {
+ return false;
+ }
+
+ final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView();
+ final ViewGroup parent = (ViewGroup) containerView.getParent();
+
+ if (parent == null) {
+ return false;
+ }
+
+ final int left = containerView.getLeft();
+ final int right = containerView.getRight();
+ final int width = right - left;
+ final boolean parentIsShown = parent.isShown();
+
+ parent.getWindowVisibleDisplayFrame(mTmpRect);
+
+ final int translateX;
+ if ((width == 0) || !parentIsShown) {
+ // not measured yet or not shown
+ translateX = (toLeft) ? (-mTmpRect.width()) : (mTmpRect.width());
+ shouldAnimate = false;
+ } else {
+ parent.getLocationInWindow(mTmpLocation);
+
+ if (toLeft) {
+ translateX = -(mTmpLocation[0] + width);
+ } else {
+ translateX = mTmpRect.width() - (mTmpLocation[0] - left);
+ }
+ }
+
+ if (shouldAnimate) {
+ shouldAnimate = containerView.isShown();
+ }
+
+ duration = (shouldAnimate) ? duration : 0;
+
+ return animateSlideInternalCompat(holder, translateX, duration, mSlideToOutsideOfWindowAnimationInterpolator);
+ }
+
+ private boolean animateSlideInternalCompat(RecyclerView.ViewHolder holder, int translationX, long duration, Interpolator interpolator) {
+ if (supportsViewPropertyAnimator()) {
+ return animateSlideInternal(holder, translationX, duration, interpolator);
+ } else {
+ return animateSlideInternalPreHoneycomb(holder, translationX);
+ }
+ }
+
+ @SuppressLint("RtlHardcoded")
+ private boolean animateSlideInternalPreHoneycomb(RecyclerView.ViewHolder holder, int translationX) {
+ if (!(holder instanceof SwipeableItemViewHolder)) {
+ return false;
+ }
+
+ final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView();
+
+ final ViewGroup.LayoutParams lp = containerView.getLayoutParams();
+ if (lp instanceof ViewGroup.MarginLayoutParams) {
+ final ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp;
+ mlp.leftMargin = translationX;
+ mlp.rightMargin = -translationX;
+
+ if (lp instanceof FrameLayout.LayoutParams) {
+ ((FrameLayout.LayoutParams) lp).gravity = Gravity.TOP | Gravity.LEFT;
+ }
+ containerView.setLayoutParams(mlp);
+ } else {
+ Log.w(TAG, "should use MarginLayoutParams supported view for compatibility on Android 2.3");
+ }
+
+ return false;
+ }
+
+ private boolean animateSlideInternal(final RecyclerView.ViewHolder holder, int translationX, long duration, Interpolator interpolator) {
+ if (!(holder instanceof SwipeableItemViewHolder)) {
+ return false;
+ }
+
+ final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView();
+
+ final int prevTranslationX = (int) (ViewCompat.getTranslationX(containerView) + 0.5f);
+
+ endAnimation(holder);
+
+ final int toX = translationX;
+
+ if (duration == 0 || Math.abs(toX - prevTranslationX) <= mImmediatelySetTranslationThreshold) {
+ ViewCompat.setTranslationX(containerView, toX);
+ return false;
+ }
+
+ ViewCompat.setTranslationX(containerView, prevTranslationX);
+
+ final ViewPropertyAnimatorCompat animator = ViewCompat.animate(containerView);
+
+ animator.setDuration(duration);
+ if (interpolator != null) {
+ animator.setInterpolator(interpolator);
+ }
+ animator.translationX(toX);
+ animator.setListener(new ViewPropertyAnimatorListener() {
+ @Override
+ public void onAnimationStart(View view) {
+ }
+
+ @Override
+ public void onAnimationEnd(View view) {
+ animator.setListener(null);
+ mActive.remove(holder);
+ ViewCompat.setTranslationX(view, toX);
+ }
+
+ @Override
+ public void onAnimationCancel(View view) {
+ }
+ });
+
+ mActive.add(holder);
+
+ animator.start();
+
+ return true;
+ }
+
+ public void endAnimation(RecyclerView.ViewHolder holder) {
+ if (!(holder instanceof SwipeableItemViewHolder)) {
+ return;
+ }
+
+ final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView();
+
+ ViewCompat.animate(containerView).cancel();
+
+ if (mActive.remove(holder)) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in the active animation list [slide]");
+ }
+ }
+
+ public void endAnimations() {
+ for (int i = mActive.size() - 1; i >= 0; i--) {
+ final RecyclerView.ViewHolder holder = mActive.get(i);
+ endAnimation(holder);
+ }
+ }
+
+ public boolean isRunning(RecyclerView.ViewHolder holder) {
+ return mActive.contains(holder);
+ }
+
+ public boolean isRunning() {
+ return !(mActive.isEmpty());
+ }
+
+ public int getImmediatelySetTranslationThreshold() {
+ return mImmediatelySetTranslationThreshold;
+ }
+
+ public void setImmediatelySetTranslationThreshold(int threshold) {
+ mImmediatelySetTranslationThreshold = threshold;
+ }
+
+ private static boolean supportsViewPropertyAnimator() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RecyclerViewSwipeManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RecyclerViewSwipeManager.java
new file mode 100644
index 00000000000..810f98835ee
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RecyclerViewSwipeManager.java
@@ -0,0 +1,750 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.swipeable;
+
+import android.graphics.Rect;
+import android.os.Build;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.ViewCompat;
+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 com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator;
+import com.h6ah4i.android.widget.advrecyclerview.utils.CustomRecyclerViewUtils;
+import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils;
+
+/**
+ * Provides item swipe operation for {@link android.support.v7.widget.RecyclerView}
+ */
+@SuppressWarnings("PointlessBitwiseExpression")
+public class RecyclerViewSwipeManager {
+ private static final String TAG = "ARVSwipeManager";
+
+ /**
+ * Result code of swipe operation. Used for the second argument of the
+ * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method.
+ *
+ * None. (internal default value, this value is not used for the argument)
+ */
+ public static final int RESULT_NONE = 0;
+
+ /**
+ * Result code of swipe operation. Used for the second argument of the
+ * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method.
+ *
+ * Canceled.
+ */
+ public static final int RESULT_CANCELED = 1;
+
+ /**
+ * Result code of swipe operation. Used for the second argument of the
+ * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method.
+ *
+ * Swipe left performed.
+ */
+ public static final int RESULT_SWIPED_LEFT = 2;
+
+ /**
+ * Result code of swipe operation. Used for the second argument of the
+ * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method.
+ *
+ * Swipe right performed.
+ */
+ public static final int RESULT_SWIPED_RIGHT = 3;
+
+ // ---
+ /**
+ * Reaction type to swipe operation. Used for the return value of the
+ * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method.
+ *
+ * Indicates "can not swipe left" (completely no reactions)
+ */
+ public static final int REACTION_CAN_NOT_SWIPE_LEFT = (0 << 0);
+
+ /**
+ * Reaction type to swipe operation. Used for the return value of the
+ * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method.
+ *
+ * Indicates "can not swipe left" (not swipeable, but rubber-band effect applied)
+ */
+ public static final int REACTION_CAN_NOT_SWIPE_LEFT_WITH_RUBBER_BAND_EFFECT = (1 << 0);
+
+ /**
+ * Reaction type to swipe operation. Used for the return value of the
+ * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method.
+ *
+ * Indicates "can swipe left"
+ */
+ public static final int REACTION_CAN_SWIPE_LEFT = (2 << 0);
+
+ /**
+ * Reaction type to swipe operation. Used for the return value of the
+ * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method.
+ *
+ * Indicates "can not swipe right" (completely no reactions)
+ */
+ public static final int REACTION_CAN_NOT_SWIPE_RIGHT = (0 << 16);
+
+ /**
+ * Reaction type to swipe operation. Used for the return value of the
+ * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method.
+ *
+ * Indicates "can not swipe right" (not swipeable, but rubber-band effect applied)
+ */
+ public static final int REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT = (1 << 16);
+
+ /**
+ * Reaction type to swipe operation. Used for the return value of the
+ * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method.
+ *
+ * Indicates "can swipe right"
+ */
+ public static final int REACTION_CAN_SWIPE_RIGHT = (2 << 16);
+
+ /**
+ * Convenient constant value: Equals to {@link #REACTION_CAN_NOT_SWIPE_LEFT} | {@link #REACTION_CAN_NOT_SWIPE_RIGHT}
+ */
+ public static final int REACTION_CAN_NOT_SWIPE_BOTH =
+ REACTION_CAN_NOT_SWIPE_LEFT | REACTION_CAN_NOT_SWIPE_RIGHT;
+
+ /**
+ * Convenient constant value: Equals to {@link #REACTION_CAN_NOT_SWIPE_LEFT_WITH_RUBBER_BAND_EFFECT} | {@link #REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT}
+ */
+ public static final int REACTION_CAN_NOT_SWIPE_BOTH_WITH_RUBBER_BAND_EFFECT =
+ REACTION_CAN_NOT_SWIPE_LEFT_WITH_RUBBER_BAND_EFFECT |
+ REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT;
+
+ /**
+ * Convenient constant value: Equals to {@link #REACTION_CAN_SWIPE_LEFT} | {@link #REACTION_CAN_SWIPE_RIGHT}
+ */
+ public static final int REACTION_CAN_SWIPE_BOTH =
+ REACTION_CAN_SWIPE_LEFT | REACTION_CAN_SWIPE_RIGHT;
+
+ // ---
+ /**
+ * Background drawable type used for the second argument of the
+ * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int)} method.
+ *
+ * Background image for the neutral (= not swiping) item.
+ */
+ public static final int DRAWABLE_SWIPE_NEUTRAL_BACKGROUND = 0;
+
+ /**
+ * Background drawable type used for the second argument of the
+ * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int)} method.
+ *
+ * Background image for the swiping-left item.
+ */
+ public static final int DRAWABLE_SWIPE_LEFT_BACKGROUND = 1;
+
+ /**
+ * Background drawable type used for the second argument of the
+ * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int)} method.
+ *
+ * Background image for the swiping-right item.
+ */
+ public static final int DRAWABLE_SWIPE_RIGHT_BACKGROUND = 2;
+
+ // ---
+ /**
+ * After-reaction type used for the {@link SwipeableItemViewHolder#setAfterSwipeReaction(int)} and {@link SwipeableItemViewHolder#getAfterSwipeReaction()} methods.
+ * Represents perform nothing.
+ */
+ public static final int AFTER_SWIPE_REACTION_DEFAULT = 0;
+
+ /**
+ * After-reaction type used for the {@link SwipeableItemViewHolder#setAfterSwipeReaction(int)} and {@link SwipeableItemViewHolder#getAfterSwipeReaction()} methods.
+ * Represents remove the swiped item.
+ */
+ public static final int AFTER_SWIPE_REACTION_REMOVE_ITEM = 1;
+
+ /**
+ * After-reaction type used for the {@link SwipeableItemViewHolder#setAfterSwipeReaction(int)} and {@link SwipeableItemViewHolder#getAfterSwipeReaction()} methods.
+ * Represents that the item moved to swiped direction.
+ */
+ public static final int AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION = 2;
+
+ // ---
+ /**
+ * Special value for the {@link SwipeableItemViewHolder#setSwipeItemSlideAmount(float)} and {@link SwipeableItemViewHolder#getSwipeItemSlideAmount()} methods.
+ * Indicates that this item is pinned to LEFT of the window.
+ */
+ public static final float OUTSIDE_OF_THE_WINDOW_LEFT = -Float.MAX_VALUE;
+
+ /**
+ * Special value for the {@link SwipeableItemViewHolder#setSwipeItemSlideAmount(float)} and {@link SwipeableItemViewHolder#getSwipeItemSlideAmount()} methods.
+ * Indicates that this item is pinned to RIGHT the window.
+ */
+ public static final float OUTSIDE_OF_THE_WINDOW_RIGHT = Float.MAX_VALUE;
+
+
+ // ---
+ /**
+ * State flag for the {@link SwipeableItemViewHolder#setSwipeStateFlags(int)} and {@link SwipeableItemViewHolder#getSwipeStateFlags()} methods.
+ * Indicates that currently performing swiping.
+ */
+ public static final int STATE_FLAG_SWIPING = (1 << 0);
+
+ /**
+ * State flag for the {@link SwipeableItemViewHolder#setSwipeStateFlags(int)} and {@link SwipeableItemViewHolder#getSwipeStateFlags()} methods.
+ * Indicates that this item is being swiped.
+ */
+ public static final int STATE_FLAG_IS_ACTIVE = (1 << 1);
+
+ /**
+ * State flag for the {@link SwipeableItemViewHolder#setSwipeStateFlags(int)} and {@link SwipeableItemViewHolder#getSwipeStateFlags()} methods.
+ * If this flag is set, some other flags are changed and require to apply.
+ */
+ public static final int STATE_FLAG_IS_UPDATED = (1 << 31);
+
+ // ---
+
+ private static final int MIN_DISTANCE_TOUCH_SLOP_MUL = 10;
+ private static final int SLIDE_ITEM_IMMEDIATELY_SET_TRANSLATION_THRESHOLD_DP = 8;
+
+ private static final boolean LOCAL_LOGV = false;
+ private static final boolean LOCAL_LOGD = false;
+
+ private RecyclerView.OnItemTouchListener mInternalUseOnItemTouchListener;
+ private RecyclerView mRecyclerView;
+
+ private long mReturnToDefaultPositionAnimationDuration = 300;
+ private long mMoveToOutsideWindowAnimationDuration = 200;
+
+ private int mTouchSlop;
+ private int mMinFlingVelocity; // [pixels per second]
+ private int mMaxFlingVelocity; // [pixels per second]
+ private int mInitialTouchX;
+ private int mInitialTouchY;
+ private long mCheckingTouchSlop = RecyclerView.NO_ID;
+
+ private ItemSlidingAnimator mItemSlideAnimator;
+ private SwipeableItemWrapperAdapter mAdapter;
+ private RecyclerView.ViewHolder mSwipingItem;
+ private Rect mSwipingItemMargins = new Rect();
+ private int mTouchedItemOffsetX;
+ private int mLastTouchX;
+ private int mSwipingItemReactionType;
+ private VelocityTracker mVelocityTracker;
+ private SwipingItemOperator mSwipingItemOperator;
+
+ /**
+ * Constructor.
+ */
+ public RecyclerViewSwipeManager() {
+ mInternalUseOnItemTouchListener = new RecyclerView.OnItemTouchListener() {
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ return RecyclerViewSwipeManager.this.onInterceptTouchEvent(rv, e);
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ RecyclerViewSwipeManager.this.onTouchEvent(rv, e);
+ }
+ };
+ mItemSlideAnimator = new ItemSlidingAnimator();
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+
+ /**
+ * Create wrapped adapter.
+ *
+ * @param adapter The target adapter.
+ *
+ * @return Wrapped adapter which is associated to this {@link RecyclerViewSwipeManager} instance.
+ */
+ @SuppressWarnings("unchecked")
+ public RecyclerView.Adapter createWrappedAdapter(RecyclerView.Adapter adapter) {
+ if (mAdapter != null) {
+ throw new IllegalStateException("already have a wrapped adapter");
+ }
+
+ mAdapter = new SwipeableItemWrapperAdapter(this, adapter);
+
+ return mAdapter;
+ }
+
+ /**
+ * Indicates this manager instance has released or not.
+ *
+ * @return True if this manager instance has released
+ */
+ public boolean isReleased() {
+ return (mInternalUseOnItemTouchListener == null);
+ }
+
+ /**
+ * Attaches {@link android.support.v7.widget.RecyclerView} instance.
+ *
+ * Before calling this method, the target {@link android.support.v7.widget.RecyclerView} must set
+ * the wrapped adapter instance which is returned by the
+ * {@link #createWrappedAdapter(android.support.v7.widget.RecyclerView.Adapter)} method.
+ *
+ * @param rv The {@link android.support.v7.widget.RecyclerView} instance
+ */
+ public void attachRecyclerView(RecyclerView rv) {
+ if (rv == null) {
+ throw new IllegalArgumentException("RecyclerView cannot be null");
+ }
+
+ if (isReleased()) {
+ throw new IllegalStateException("Accessing released object");
+ }
+
+ if (mRecyclerView != null) {
+ throw new IllegalStateException("RecyclerView instance has already been set");
+ }
+
+ if (mAdapter == null || getSwipeableItemWrapperAdapter(rv) != mAdapter) {
+ throw new IllegalStateException("adapter is not set properly");
+ }
+
+ mRecyclerView = rv;
+ mRecyclerView.addOnItemTouchListener(mInternalUseOnItemTouchListener);
+
+ final ViewConfiguration vc = ViewConfiguration.get(rv.getContext());
+
+ mTouchSlop = vc.getScaledTouchSlop();
+ mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
+ mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
+
+ mItemSlideAnimator.setImmediatelySetTranslationThreshold(
+ (int) (rv.getResources().getDisplayMetrics().density * SLIDE_ITEM_IMMEDIATELY_SET_TRANSLATION_THRESHOLD_DP + 0.5f));
+ }
+
+ /**
+ * Detach the {@link android.support.v7.widget.RecyclerView} instance and release internal field references.
+ *
+ * This method should be called in order to avoid memory leaks.
+ */
+ public void release() {
+ if (mRecyclerView != null && mInternalUseOnItemTouchListener != null) {
+ mRecyclerView.removeOnItemTouchListener(mInternalUseOnItemTouchListener);
+ }
+ mInternalUseOnItemTouchListener = null;
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+
+ if (mItemSlideAnimator != null) {
+ mItemSlideAnimator.endAnimations();
+ mItemSlideAnimator = null;
+ }
+
+ mAdapter = null;
+ mRecyclerView = null;
+ }
+
+ /**
+ * Indicates whether currently performing item swiping.
+ *
+ * @return True if currently performing item swiping
+ */
+ public boolean isSwiping() {
+ return (mSwipingItem != null);
+ }
+
+ /*package*/ boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ final int action = MotionEventCompat.getActionMasked(e);
+
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "onInterceptTouchEvent() action = " + action);
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ if (isSwiping()) {
+ handleActionUpOrCancelWhileSwiping(e);
+ return true;
+ } else {
+ handleActionUpOrCancelWhileNotSwiping();
+ }
+ break;
+
+ case MotionEvent.ACTION_DOWN:
+ if (!isSwiping()) {
+ handleActionDown(rv, e);
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (isSwiping()) {
+ // NOTE: The first ACTION_MOVE event will come here. (maybe a bug of RecyclerView?)
+ handleActionMoveWhileSwiping(e);
+ return true;
+ } else {
+ if (handleActionMoveWhileNotSwiping(rv, e)) {
+ return true;
+ }
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ /*package*/ void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ final int action = MotionEventCompat.getActionMasked(e);
+
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "onTouchEvent() action = " + action);
+ }
+
+ if (!isSwiping()) {
+ // Log.w(TAG, "onTouchEvent() - unexpected state");
+ return;
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ handleActionUpOrCancelWhileSwiping(e);
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ handleActionMoveWhileSwiping(e);
+ break;
+ }
+ }
+
+ private boolean handleActionDown(RecyclerView rv, MotionEvent e) {
+ final RecyclerView.Adapter adapter = rv.getAdapter();
+ final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithTranslation(rv, e.getX(), e.getY());
+
+ if (!(holder instanceof SwipeableItemViewHolder)) {
+ return false;
+ }
+
+ final int itemPosition = holder.getPosition();
+
+ // verify the touched item is valid state
+ if (!(itemPosition >= 0 && itemPosition < adapter.getItemCount())) {
+ return false;
+ }
+ if (holder.getItemId() != adapter.getItemId(itemPosition)) {
+ return false;
+ }
+
+ final int touchX = (int) (e.getX() + 0.5f);
+ final int touchY = (int) (e.getY() + 0.5f);
+
+ final View view = holder.itemView;
+ final int translateX = (int) (ViewCompat.getTranslationX(view) + 0.5f);
+ final int translateY = (int) (ViewCompat.getTranslationY(view) + 0.5f);
+ final int viewX = touchX - (view.getLeft() + translateX);
+ final int viewY = touchY - (view.getTop() + translateY);
+
+ final int reactionType = mAdapter.getSwipeReactionType(holder, viewX, viewY);
+
+ if (reactionType == 0) {
+ return false;
+ }
+
+ mInitialTouchX = touchX;
+ mInitialTouchY = touchY;
+ mCheckingTouchSlop = holder.getItemId();
+ mSwipingItemReactionType = reactionType;
+
+ return true;
+ }
+
+ private static SwipeableItemWrapperAdapter getSwipeableItemWrapperAdapter(RecyclerView rv) {
+ return WrapperAdapterUtils.findWrappedAdapter(rv.getAdapter(), SwipeableItemWrapperAdapter.class);
+ }
+
+ private void handleActionUpOrCancelWhileNotSwiping() {
+ mCheckingTouchSlop = RecyclerView.NO_ID;
+ mSwipingItemReactionType = 0;
+ }
+
+ private boolean handleActionUpOrCancelWhileSwiping(MotionEvent e) {
+ mLastTouchX = (int) (e.getX() + 0.5f);
+
+ int result = RESULT_CANCELED;
+
+ if (MotionEventCompat.getActionMasked(e) == MotionEvent.ACTION_UP) {
+ final int viewWidth = mSwipingItem.itemView.getWidth();
+ final float distance = mLastTouchX - mInitialTouchX;
+ final float absDistance = Math.abs(distance);
+
+ mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity); // 1000: pixels per second
+
+ final float xVelocity = mVelocityTracker.getXVelocity();
+ final float absXVelocity = Math.abs(xVelocity);
+
+ if ((absDistance > (mTouchSlop * MIN_DISTANCE_TOUCH_SLOP_MUL)) &&
+ ((distance * xVelocity) > 0.0f) &&
+ (absXVelocity <= mMaxFlingVelocity) &&
+ ((absDistance > (viewWidth / 2)) || (absXVelocity >= mMinFlingVelocity))) {
+
+ if ((distance < 0) && SwipeReactionUtils.canSwipeLeft(mSwipingItemReactionType)) {
+ result = RESULT_SWIPED_LEFT;
+ } else if ((distance > 0) && SwipeReactionUtils.canSwipeRight(mSwipingItemReactionType)) {
+ result = RESULT_SWIPED_RIGHT;
+ }
+ }
+ }
+
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "swping finished --- result = " + result);
+ }
+
+ finishSwiping(result);
+
+ return true;
+ }
+
+ private boolean handleActionMoveWhileNotSwiping(RecyclerView rv, MotionEvent e) {
+ if (mCheckingTouchSlop == RecyclerView.NO_ID) {
+ return false;
+ }
+
+ if (Math.abs(e.getY() - mInitialTouchY) > mTouchSlop) {
+ // scrolling occurred
+ mCheckingTouchSlop = RecyclerView.NO_ID;
+ return false;
+ }
+
+ if (Math.abs(e.getX() - mInitialTouchX) <= mTouchSlop) {
+ return false;
+ }
+
+ final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithTranslation(rv, e.getX(), e.getY());
+
+ if (holder == null || holder.getItemId() != mCheckingTouchSlop) {
+ mCheckingTouchSlop = RecyclerView.NO_ID;
+ return false;
+ }
+
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "swiping started");
+ }
+
+ startSwiping(rv, e, holder);
+
+ return true;
+ }
+
+ private void handleActionMoveWhileSwiping(MotionEvent e) {
+ mLastTouchX = (int) (e.getX() + 0.5f);
+ mVelocityTracker.addMovement(e);
+
+ final int swipeDistance = mLastTouchX - mTouchedItemOffsetX;
+
+ mSwipingItemOperator.update(swipeDistance);
+ }
+
+ private void startSwiping(RecyclerView rv, MotionEvent e, RecyclerView.ViewHolder holder) {
+ mSwipingItem = holder;
+ mLastTouchX = (int) (e.getX() + 0.5f);
+ mTouchedItemOffsetX = mLastTouchX;
+ CustomRecyclerViewUtils.getLayoutMargins(holder.itemView, mSwipingItemMargins);
+
+ mSwipingItemOperator = new SwipingItemOperator(this, mSwipingItem, mSwipingItemReactionType);
+ mSwipingItemOperator.start();
+
+ mVelocityTracker.clear();
+ mVelocityTracker.addMovement(e);
+
+ mRecyclerView.getParent().requestDisallowInterceptTouchEvent(true);
+
+ // raise onSwipeItemStarted() event
+ mAdapter.onSwipeItemStarted(this, holder);
+ }
+
+ private void finishSwiping(int result) {
+ final RecyclerView.ViewHolder swipingItem = mSwipingItem;
+
+ if (swipingItem == null) {
+ return;
+ }
+
+ if (mRecyclerView != null && mRecyclerView.getParent() != null) {
+ mRecyclerView.getParent().requestDisallowInterceptTouchEvent(false);
+ }
+
+ mVelocityTracker.clear();
+
+ mSwipingItem = null;
+ mLastTouchX = 0;
+ mInitialTouchX = 0;
+ mTouchedItemOffsetX = 0;
+ mCheckingTouchSlop = RecyclerView.NO_ID;
+ mSwipingItemReactionType = 0;
+
+ if (mSwipingItemOperator != null) {
+ mSwipingItemOperator.finish();
+ mSwipingItemOperator = null;
+ }
+
+ final boolean toLeft = (result == RESULT_SWIPED_LEFT);
+ int afterReaction = AFTER_SWIPE_REACTION_DEFAULT;
+
+ if (mAdapter != null) {
+ afterReaction = mAdapter.onSwipeItemFinished(swipingItem, result);
+ }
+
+ afterReaction = correctAfterReaction(result, afterReaction);
+
+ switch (afterReaction) {
+ case AFTER_SWIPE_REACTION_DEFAULT:
+ mItemSlideAnimator.slideToDefaultPosition(swipingItem, true, mReturnToDefaultPositionAnimationDuration);
+ break;
+ case AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION:
+ mItemSlideAnimator.slideToOutsideOfWindow(
+ swipingItem, toLeft, true, mMoveToOutsideWindowAnimationDuration);
+ break;
+ case AFTER_SWIPE_REACTION_REMOVE_ITEM: {
+ final RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator();
+
+ final long removeAnimationDuration = (itemAnimator != null) ? itemAnimator.getRemoveDuration() : 0;
+
+ if (supportsViewPropertyAnimator()) {
+ final long moveAnimationDuration = (itemAnimator != null) ? itemAnimator.getMoveDuration() : 0;
+
+ final RemovingItemDecorator decorator = new RemovingItemDecorator(
+ mRecyclerView, swipingItem, removeAnimationDuration, moveAnimationDuration);
+
+ decorator.setMoveAnimationInterpolator(SwipeDismissItemAnimator.MOVE_INTERPOLATOR);
+ decorator.start();
+ }
+
+ mItemSlideAnimator.slideToOutsideOfWindow(
+ swipingItem, toLeft, true, removeAnimationDuration);
+ }
+ break;
+ default:
+ throw new IllegalStateException("Unknwon after reaction type: " + afterReaction);
+ }
+
+ if (mAdapter != null) {
+ mAdapter.onSwipeItemFinished2(swipingItem, result, afterReaction);
+ }
+ }
+
+ private static int correctAfterReaction(int result, int afterReaction) {
+ if ((afterReaction == AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION) ||
+ (afterReaction == AFTER_SWIPE_REACTION_REMOVE_ITEM)) {
+ if (!((result == RESULT_SWIPED_LEFT) || (result == RESULT_SWIPED_RIGHT))) {
+ afterReaction = AFTER_SWIPE_REACTION_DEFAULT;
+ }
+ }
+
+ return afterReaction;
+ }
+
+ public void cancelSwipe() {
+ finishSwiping(RESULT_CANCELED);
+ }
+
+ /*package*/ boolean isAnimationRunning(RecyclerView.ViewHolder item) {
+ return (mItemSlideAnimator != null) && (mItemSlideAnimator.isRunning(item));
+ }
+
+ private void slideItem(RecyclerView.ViewHolder holder, float amount, boolean shouldAnimate) {
+ if (amount == OUTSIDE_OF_THE_WINDOW_LEFT) {
+ mItemSlideAnimator.slideToOutsideOfWindow(holder, true, shouldAnimate, mMoveToOutsideWindowAnimationDuration);
+ } else if (amount == OUTSIDE_OF_THE_WINDOW_RIGHT) {
+ mItemSlideAnimator.slideToOutsideOfWindow(holder, false, shouldAnimate, mMoveToOutsideWindowAnimationDuration);
+ } else if (amount == 0.0f) {
+ mItemSlideAnimator.slideToDefaultPosition(holder, shouldAnimate, mReturnToDefaultPositionAnimationDuration);
+ } else {
+ mItemSlideAnimator.slideToSpecifiedPosition(holder, amount);
+ }
+ }
+
+ /**
+ * Gets the duration of the "return to default position" animation
+ *
+ * @return Duration of the "return to default position" animation in milliseconds
+ */
+ public long getReturnToDefaultPositionAnimationDuration() {
+ return mReturnToDefaultPositionAnimationDuration;
+ }
+
+ /**
+ * Sets the duration of the "return to default position" animation
+ *
+ * @param duration Duration of the "return to default position" animation in milliseconds
+ */
+ public void setReturnToDefaultPositionAnimationDuration(long duration) {
+ mReturnToDefaultPositionAnimationDuration = duration;
+ }
+
+ /**
+ * Gets the duration of the "move to outside of the window" animation
+ *
+ * @return Duration of the "move to outside of the window" animation in milliseconds
+ */
+ public long getMoveToOutsideWindowAnimationDuration() {
+ return mMoveToOutsideWindowAnimationDuration;
+ }
+
+ /**
+ * Sets the duration of the "move to outside of the window" animation
+ *
+ * @param duration Duration of the "move to outside of the window" animation in milliseconds
+ */
+ public void setMoveToOutsideWindowAnimationDuration(long duration) {
+ mMoveToOutsideWindowAnimationDuration = duration;
+ }
+
+ /*package*/ void applySlideItem(RecyclerView.ViewHolder holder, float prevAmount, float amount, boolean shouldAnimate) {
+ final SwipeableItemViewHolder holder2 = (SwipeableItemViewHolder) holder;
+ final View containerView = holder2.getSwipeableContainerView();
+
+ if (containerView == null) {
+ return;
+ }
+
+ final int reqBackgroundType;
+
+ if (amount == 0.0f) {
+ if (prevAmount == 0.0f) {
+ reqBackgroundType = DRAWABLE_SWIPE_NEUTRAL_BACKGROUND;
+ } else {
+ reqBackgroundType = (prevAmount < 0)
+ ? DRAWABLE_SWIPE_LEFT_BACKGROUND
+ : DRAWABLE_SWIPE_RIGHT_BACKGROUND;
+ }
+ } else {
+ reqBackgroundType = (amount < 0)
+ ? DRAWABLE_SWIPE_LEFT_BACKGROUND
+ : DRAWABLE_SWIPE_RIGHT_BACKGROUND;
+ }
+
+ if (amount == 0.0f) {
+ slideItem(holder, amount, shouldAnimate);
+ mAdapter.setSwipeBackgroundDrawable(holder, reqBackgroundType);
+ } else {
+ mAdapter.setSwipeBackgroundDrawable(holder, reqBackgroundType);
+ slideItem(holder, amount, shouldAnimate);
+ }
+ }
+
+ private static boolean supportsViewPropertyAnimator() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RemovingItemDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RemovingItemDecorator.java
new file mode 100644
index 00000000000..c79dc333f20
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RemovingItemDecorator.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.swipeable;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.h6ah4i.android.widget.advrecyclerview.utils.CustomRecyclerViewUtils;
+
+import java.lang.ref.WeakReference;
+
+class RemovingItemDecorator extends RecyclerView.ItemDecoration {
+ @SuppressWarnings("unused")
+ private static final String TAG = "RemovingItemDecorator";
+
+ private static final int NOTIFY_REMOVAL_EFFECT_PHASE_1 = 0;
+ private static final int NOTIFY_REMOVAL_EFFECT_END = 1;
+
+ private static final long ADDITIONAL_REMOVE_DURATION = 100; // workaround: to avoid the gap between the below item
+
+ private RecyclerView mRecyclerView;
+ private RecyclerView.ViewHolder mSwipingItem;
+ private long mSwipingItemId;
+ private Rect mSwipingItemBounds = new Rect();
+ private int mInitialTranslationX;
+ private int mTranslationY;
+ private long mStartTime;
+ private long mRemoveAnimationDuration;
+ private long mMoveAnimationDuration;
+ private Interpolator mMoveAnimationInterpolator;
+ private Drawable mSwipeBackgroundDrawable;
+
+ private int mPendingNotificationMask = 0;
+
+ public RemovingItemDecorator(RecyclerView rv, RecyclerView.ViewHolder swipingItem, long removeAnimationDuration, long moveAnimationDuration) {
+ mRecyclerView = rv;
+ mSwipingItem = swipingItem;
+ mSwipingItemId = swipingItem.getItemId();
+
+ mRemoveAnimationDuration = removeAnimationDuration + ADDITIONAL_REMOVE_DURATION;
+ mMoveAnimationDuration = moveAnimationDuration;
+ mInitialTranslationX = (int) (ViewCompat.getTranslationX(swipingItem.itemView) + 0.5f);
+
+ CustomRecyclerViewUtils.getViewBounds(mSwipingItem.itemView, mSwipingItemBounds);
+ }
+
+ public void setMoveAnimationInterpolator(Interpolator interpolator) {
+ mMoveAnimationInterpolator = interpolator;
+ }
+
+ @Override
+ public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ final long elapsedTime = getElapsedTime(mStartTime);
+
+ final Rect bounds = mSwipingItemBounds;
+ final float heightScale = determineBackgroundHeightScaleSwipeCompletedSuccessfully(elapsedTime);
+
+ final int origHeight = bounds.height();
+ final int actualHeight = (int) (heightScale * origHeight + 0.5f);
+
+ fillSwipingItemBackground(c, mSwipeBackgroundDrawable, actualHeight);
+
+ if (mSwipingItemId == mSwipingItem.getItemId()) {
+ mTranslationY = (int) (ViewCompat.getTranslationY(mSwipingItem.itemView) + 0.5f);
+ }
+
+ if (requiresContinuousAnimationAfterSwipeCompletedSuccessfully(elapsedTime)) {
+ postInvalidateOnAnimation();
+ }
+ }
+
+ private boolean requiresContinuousAnimationAfterSwipeCompletedSuccessfully(long elapsedTime) {
+ return (elapsedTime >= mRemoveAnimationDuration) &&
+ (elapsedTime < (mRemoveAnimationDuration + mMoveAnimationDuration));
+ }
+
+ private float determineBackgroundHeightScaleSwipeCompletedSuccessfully(long elapsedTime) {
+ float heightScale = 0.0f;
+
+ if (elapsedTime < mRemoveAnimationDuration) {
+ heightScale = 1.0f;
+ } else if (elapsedTime < (mRemoveAnimationDuration + mMoveAnimationDuration)) {
+ if (mMoveAnimationDuration != 0) {
+ heightScale = 1.0f - (float) (elapsedTime - mRemoveAnimationDuration) / mMoveAnimationDuration;
+ if (mMoveAnimationInterpolator != null) {
+ heightScale = mMoveAnimationInterpolator.getInterpolation(heightScale);
+ }
+ }
+ }
+
+ return heightScale;
+ }
+
+ private void fillSwipingItemBackground(Canvas c, Drawable drawable, int height) {
+ final Rect bounds = mSwipingItemBounds;
+ final int translationX = mInitialTranslationX;
+ final int translationY = mTranslationY;
+
+ if (height < 0) {
+ height = bounds.height();
+ }
+
+ if ((height == 0) || (drawable == null)) {
+ return;
+ }
+
+ final int savedCount = c.save();
+
+ c.clipRect(
+ bounds.left + translationX,
+ bounds.top + translationY,
+ bounds.right + translationX,
+ bounds.top + translationY + height);
+
+ // c.drawColor(0xffff0000); // <-- debug
+
+ c.translate(
+ bounds.left + translationX,
+ bounds.top + translationY - (bounds.height() - height) / 2);
+ drawable.setBounds(0, 0, bounds.width(), bounds.height());
+
+ drawable.draw(c);
+
+ c.restoreToCount(savedCount);
+ }
+
+ private void postInvalidateOnAnimation() {
+// final Rect bounds = mSwipingItemBounds;
+// final int translationX = mInitialTranslationX;
+// final int translationY = mTranslationY;
+
+// ViewCompat.postInvalidateOnAnimation(
+// mRecyclerView,
+// bounds.left + translationX,
+// bounds.top + translationY,
+// bounds.right + translationX,
+// bounds.bottom + translationY);
+ ViewCompat.postInvalidateOnAnimation(mRecyclerView);
+ }
+
+ public void start() {
+ final View containerView = ((SwipeableItemViewHolder) mSwipingItem).getSwipeableContainerView();
+
+ ViewCompat.animate(containerView).cancel();
+
+ mRecyclerView.addItemDecoration(this);
+
+ mStartTime = System.currentTimeMillis();
+ mTranslationY = (int) (ViewCompat.getTranslationY(mSwipingItem.itemView) + 0.5f);
+ mSwipeBackgroundDrawable = mSwipingItem.itemView.getBackground();
+
+ postInvalidateOnAnimation();
+ notifyDelayed(NOTIFY_REMOVAL_EFFECT_PHASE_1, mRemoveAnimationDuration);
+ }
+
+ private void notifyDelayed(int code, long delay) {
+ final int mask = (1 << code);
+
+ if ((mPendingNotificationMask & mask) != 0) {
+ return;
+ }
+
+ mPendingNotificationMask |= mask;
+
+ final DelayedNotificationRunner notification = new DelayedNotificationRunner(this, code);
+ ViewCompat.postOnAnimationDelayed(mRecyclerView, notification, delay);
+ }
+
+ /*package*/ void onDelayedNotification(int code) {
+ final int mask = (1 << code);
+ final long elapsedTime = getElapsedTime(mStartTime);
+
+ mPendingNotificationMask &= (~mask);
+
+ switch (code) {
+ case NOTIFY_REMOVAL_EFFECT_PHASE_1:
+ if (elapsedTime < mRemoveAnimationDuration) {
+ notifyDelayed(NOTIFY_REMOVAL_EFFECT_PHASE_1, (mRemoveAnimationDuration - elapsedTime));
+ } else {
+ postInvalidateOnAnimation();
+ notifyDelayed(NOTIFY_REMOVAL_EFFECT_END, mMoveAnimationDuration);
+ }
+ break;
+ case NOTIFY_REMOVAL_EFFECT_END:
+ finish();
+ break;
+ }
+ }
+
+ private void finish() {
+ mRecyclerView.removeItemDecoration(this);
+ postInvalidateOnAnimation(); // this is required to avoid remnant of the decoration
+
+ mRecyclerView = null;
+ mSwipingItem = null;
+ mTranslationY = 0;
+ mMoveAnimationInterpolator = null;
+ }
+
+ protected static long getElapsedTime(long initialTime) {
+ final long curTime = System.currentTimeMillis();
+ return (curTime >= initialTime) ? (curTime - initialTime) : Long.MAX_VALUE;
+ }
+
+ private static class DelayedNotificationRunner implements Runnable {
+ private WeakReference mRefDecorator;
+ private final int mCode;
+
+ public DelayedNotificationRunner(RemovingItemDecorator decorator, int code) {
+ mRefDecorator = new WeakReference<>(decorator);
+ mCode = code;
+ }
+
+ @Override
+ public void run() {
+ final RemovingItemDecorator decorator = mRefDecorator.get();
+ mRefDecorator.clear();
+ mRefDecorator = null;
+
+ if (decorator != null) {
+ decorator.onDelayedNotification(mCode);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RubberBandInterpolator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RubberBandInterpolator.java
new file mode 100644
index 00000000000..0c97a1afdcf
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RubberBandInterpolator.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.swipeable;
+
+import android.view.animation.Interpolator;
+
+class RubberBandInterpolator implements Interpolator {
+ private final float mLimit;
+
+ public RubberBandInterpolator(float limit) {
+ mLimit = limit;
+ }
+
+ @Override
+ public float getInterpolation(float input) {
+ final float t = (1 - input);
+ return mLimit * (1 - (t * t));
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeReactionUtils.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeReactionUtils.java
new file mode 100644
index 00000000000..5caa0c5aef9
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeReactionUtils.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.swipeable;
+
+class SwipeReactionUtils {
+ public static final int REACTION_CAN_NOT_SWIPE = 0;
+ public static final int REACTION_CAN_NOT_SWIPE_WITH_RUBBER_EFFECT = 1;
+ public static final int REACTION_CAN_SWIPE = 2;
+
+ public static int extractLeftReaction(int type) {
+ return (type & 0x3);
+ }
+
+ public static int extractRightReaction(int type) {
+ return ((type >>> 16) & 0x3);
+ }
+
+ public static boolean canSwipeLeft(int reactionType) {
+ return (extractLeftReaction(reactionType) == REACTION_CAN_SWIPE);
+ }
+
+ public static boolean canSwipeRight(int reactionType) {
+ return (extractRightReaction(reactionType) == REACTION_CAN_SWIPE);
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemAdapter.java
new file mode 100644
index 00000000000..8065322bf76
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemAdapter.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.swipeable;
+
+import android.support.v7.widget.RecyclerView;
+
+public interface SwipeableItemAdapter {
+
+ /**
+ * Called when user is attempt to swipe the item.
+ *
+ * @param holder The ViewHolder which is associated to item user is attempt to start swiping.
+ * @param x Touched X position. Relative from the itemView's top-left.
+ * @param y Touched Y position. Relative from the itemView's top-left.
+
+ * @return Reaction type. Bitwise OR of these flags;
+ * - {@link RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_LEFT}
+ * - {@link RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_LEFT_WITH_RUBBER_BAND_EFFECT}
+ * - {@link RecyclerViewSwipeManager#REACTION_CAN_SWIPE_LEFT}
+ * - {@link RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_RIGHT}
+ * - {@link RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT}
+ * - {@link RecyclerViewSwipeManager#REACTION_CAN_SWIPE_RIGHT}
+ */
+ int onGetSwipeReactionType(T holder, int x, int y);
+
+ /**
+ * Called when sets background of the swiping item.
+ *
+ * @param holder The ViewHolder which is associated to the swiping item.
+ * @param type Background type. One of the
+ * {@link RecyclerViewSwipeManager#DRAWABLE_SWIPE_NEUTRAL_BACKGROUND},
+ * {@link RecyclerViewSwipeManager#DRAWABLE_SWIPE_LEFT_BACKGROUND} or
+ * {@link RecyclerViewSwipeManager#DRAWABLE_SWIPE_RIGHT_BACKGROUND}.
+ */
+ void onSetSwipeBackground(T holder, int type);
+
+ /**
+ * Called when item is swiped.
+ *
+ * *Note that do not change data set and do not call notifyDataXXX() methods inside of this method.*
+ *
+ * @param holder The ViewHolder which is associated to the swiped item.
+ * @param result The result code of user's swipe operation.
+ * {@link RecyclerViewSwipeManager#RESULT_CANCELED},
+ * {@link RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or
+ * {@link RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT}
+ *
+ * @return Reaction type of after swiping.
+ * One of the {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT},
+ * {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} or
+ * {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}.
+ */
+ int onSwipeItem(T holder, int result);
+
+ /**
+ * Called after {@link #onSwipeItem(android.support.v7.widget.RecyclerView.ViewHolder, int)} method.
+ *
+ * You can update data set and call notifyDataXXX() methods inside of this method.
+ *
+ * @param holder The ViewHolder which is associated to the swiped item.
+ * @param result The result code of user's swipe operation.
+ * {@link RecyclerViewSwipeManager#RESULT_CANCELED},
+ * {@link RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or
+ * {@link RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT}
+ * @param reaction Reaction type. One of the {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT},
+ * {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} or
+ * {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}.
+ */
+ void onPerformAfterSwipeReaction(T holder, int result, int reaction);
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemViewHolder.java
new file mode 100644
index 00000000000..5c4714364dc
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemViewHolder.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.swipeable;
+
+import android.view.View;
+
+/**
+ * Interface which provides required information for swiping item.
+ *
+ * Implement this interface on your sub-class of the {@link android.support.v7.widget.RecyclerView.ViewHolder}.
+ */
+public interface SwipeableItemViewHolder {
+ /**
+ * Sets the state flags value for swiping item
+ *
+ * @param flags Bitwise OR of these flags;
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#STATE_FLAG_SWIPING}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#STATE_FLAG_IS_ACTIVE}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#STATE_FLAG_IS_UPDATED}
+ */
+ void setSwipeStateFlags(int flags);
+
+ /**
+ * Gets the state flags value for swiping item
+ *
+ * @return Bitwise OR of these flags;
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#STATE_FLAG_SWIPING}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#STATE_FLAG_IS_ACTIVE}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#STATE_FLAG_IS_UPDATED}
+ */
+ int getSwipeStateFlags();
+
+ /**
+ * Sets the result code of swiping item.
+ *
+ * @param result Result code. One of these values;
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_NONE}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_LEFT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_CANCELED}
+ */
+ void setSwipeResult(int result);
+
+ /**
+ * Gets the result code of swiping item.
+ *
+ * @return Result code. One of these values;
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_NONE}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_LEFT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_SWIPED_RIGHT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#RESULT_CANCELED}
+ */
+ int getSwipeResult();
+
+ /**
+ * Sets the reaction type of after swiping item.
+ *
+ * @param reaction After-reaction type. One of these values;
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION}
+ */
+ void setAfterSwipeReaction(int reaction);
+
+ /**
+ * Gets the reaction type of after swiping item.
+ *
+ * @return After-reaction type. One of these values;
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_DEFAULT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION}
+ */
+ int getAfterSwipeReaction();
+
+ /**
+ * Sets the item swipe amount.
+ *
+ * @param amount Item swipe amount. Generally the range is [-1.0 .. 1.0], but these special values can be accepted;
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#OUTSIDE_OF_THE_WINDOW_LEFT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#OUTSIDE_OF_THE_WINDOW_RIGHT}
+ */
+ void setSwipeItemSlideAmount(float amount);
+
+ /**
+ * Gets the item swipe amount.
+ *
+ * @return Item swipe amount. Generally the range is [-1.0 .. 1.0], but these special values may be returned;
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#OUTSIDE_OF_THE_WINDOW_LEFT}
+ * - {@link com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager#OUTSIDE_OF_THE_WINDOW_RIGHT}
+ */
+ float getSwipeItemSlideAmount();
+
+ /**
+ * Gets the container view for the swipeable area.
+ *
+ * @return The container view instance.
+ */
+ View getSwipeableContainerView();
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemWrapperAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemWrapperAdapter.java
new file mode 100644
index 00000000000..6cc6ee9607a
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemWrapperAdapter.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.swipeable;
+
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.h6ah4i.android.widget.advrecyclerview.utils.BaseWrapperAdapter;
+import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils;
+
+class SwipeableItemWrapperAdapter extends BaseWrapperAdapter {
+ private static final String TAG = "ARVSwipeableWrapper";
+
+ private static final int STATE_FLAG_INITIAL_VALUE = -1;
+
+ private static final boolean LOCAL_LOGV = false;
+ private static final boolean LOCAL_LOGD = false;
+
+ private SwipeableItemAdapter mSwipeableItemAdapter;
+ private RecyclerViewSwipeManager mSwipeManager;
+ private int mSwipingItemPosition = RecyclerView.NO_POSITION;
+
+ public SwipeableItemWrapperAdapter(RecyclerViewSwipeManager manager, RecyclerView.Adapter adapter) {
+ super(adapter);
+
+ mSwipeableItemAdapter = getSwipeableItemAdapter(adapter);
+ if (mSwipeableItemAdapter == null) {
+ throw new IllegalArgumentException("adapter does not implement SwipeableItemAdapter");
+ }
+
+ if (manager == null) {
+ throw new IllegalArgumentException("manager cannot be null");
+ }
+
+ mSwipeManager = manager;
+ }
+
+ @Override
+ protected void onRelease() {
+ super.onRelease();
+
+ mSwipeableItemAdapter = null;
+ mSwipeManager = null;
+ mSwipingItemPosition = RecyclerView.NO_POSITION;
+ }
+
+ @Override
+ public void onViewRecycled(VH holder) {
+ super.onViewRecycled(holder);
+
+ // reset SwipeableItemViewHolder state
+ if (holder instanceof SwipeableItemViewHolder) {
+ ((SwipeableItemViewHolder) holder).setSwipeItemSlideAmount(0.0f);
+
+ View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView();
+
+ if (containerView != null) {
+ ViewCompat.animate(containerView).cancel();
+ ViewCompat.setTranslationX(containerView, 0.0f);
+ }
+ }
+ }
+
+ @Override
+ public VH onCreateViewHolder(ViewGroup parent, int viewType) {
+ final VH holder = super.onCreateViewHolder(parent, viewType);
+
+ if (holder instanceof SwipeableItemViewHolder) {
+ ((SwipeableItemViewHolder) holder).setSwipeStateFlags(STATE_FLAG_INITIAL_VALUE);
+ }
+
+ return holder;
+ }
+
+ @Override
+ public void onBindViewHolder(VH holder, int position) {
+ float prevSwipeItemSlideAmount = 0;
+
+ if (holder instanceof SwipeableItemViewHolder) {
+ prevSwipeItemSlideAmount = ((SwipeableItemViewHolder) holder).getSwipeItemSlideAmount();
+ }
+
+ if (isSwiping()) {
+ int flags = RecyclerViewSwipeManager.STATE_FLAG_SWIPING;
+
+ if (position == mSwipingItemPosition) {
+ flags |= RecyclerViewSwipeManager.STATE_FLAG_IS_ACTIVE;
+ }
+
+ safeUpdateFlags(holder, flags);
+ super.onBindViewHolder(holder, position);
+ } else {
+ safeUpdateFlags(holder, 0);
+ super.onBindViewHolder(holder, position);
+ }
+
+ if (holder instanceof SwipeableItemViewHolder) {
+ final float swipeItemSlideAmount = ((SwipeableItemViewHolder) holder).getSwipeItemSlideAmount();
+
+ if ((prevSwipeItemSlideAmount != swipeItemSlideAmount) ||
+ !(mSwipeManager.isSwiping() || mSwipeManager.isAnimationRunning(holder))) {
+ mSwipeManager.applySlideItem(holder, prevSwipeItemSlideAmount, swipeItemSlideAmount, true);
+ }
+ }
+ }
+
+ @Override
+ protected void onHandleWrappedAdapterChanged() {
+ if (isSwiping()) {
+ cancelSwipe();
+ } else {
+ super.onHandleWrappedAdapterChanged();
+ }
+ }
+
+ @Override
+ protected void onHandleWrappedAdapterItemRangeChanged(int positionStart, int itemCount) {
+ if (isSwiping()) {
+ cancelSwipe();
+ } else {
+ super.onHandleWrappedAdapterItemRangeChanged(positionStart, itemCount);
+ }
+ }
+
+ @Override
+ protected void onHandleWrappedAdapterItemRangeInserted(int positionStart, int itemCount) {
+ if (isSwiping()) {
+ cancelSwipe();
+ } else {
+ super.onHandleWrappedAdapterItemRangeInserted(positionStart, itemCount);
+ }
+ }
+
+ @Override
+ protected void onHandleWrappedAdapterItemRangeRemoved(int positionStart, int itemCount) {
+ if (isSwiping()) {
+ cancelSwipe();
+ } else {
+ super.onHandleWrappedAdapterItemRangeRemoved(positionStart, itemCount);
+ }
+ }
+
+ @Override
+ protected void onHandleWrappedAdapterRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ if (isSwiping()) {
+ cancelSwipe();
+ } else {
+ super.onHandleWrappedAdapterRangeMoved(fromPosition, toPosition, itemCount);
+ }
+ }
+
+ private void cancelSwipe() {
+ if (mSwipeManager != null) {
+ mSwipeManager.cancelSwipe();
+ }
+ }
+
+ // NOTE: This method is called from RecyclerViewDragDropManager
+ /*package*/
+ @SuppressWarnings("unchecked")
+ int getSwipeReactionType(RecyclerView.ViewHolder holder, int x, int y) {
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "getSwipeReactionType(holder = " + holder + ", x = " + x + ", y = " + y + ")");
+ }
+
+ return mSwipeableItemAdapter.onGetSwipeReactionType(holder, x, y);
+ }
+
+ // NOTE: This method is called from RecyclerViewDragDropManager
+ /*package*/
+ @SuppressWarnings("unchecked")
+ void setSwipeBackgroundDrawable(RecyclerView.ViewHolder holder, int type) {
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "setSwipeBackgroundDrawable(holder = " + holder + ", type = " + type + ")");
+ }
+
+ mSwipeableItemAdapter.onSetSwipeBackground(holder, type);
+ }
+
+ // NOTE: This method is called from RecyclerViewDragDropManager
+ /*package*/ void onSwipeItemStarted(RecyclerViewSwipeManager manager, RecyclerView.ViewHolder holder) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "onSwipeItemStarted(holder = " + holder + ")");
+ }
+
+ mSwipingItemPosition = holder.getPosition();
+
+ notifyDataSetChanged();
+ }
+
+ // NOTE: This method is called from RecyclerViewDragDropManager
+ /*package*/
+ @SuppressWarnings("unchecked")
+ int onSwipeItemFinished(RecyclerView.ViewHolder holder, int result) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "onSwipeItemFinished(holder = " + holder + ", result = " + result + ")");
+ }
+
+ mSwipingItemPosition = RecyclerView.NO_POSITION;
+
+ return mSwipeableItemAdapter.onSwipeItem(holder, result);
+ }
+
+ /*package*/
+ @SuppressWarnings("unchecked")
+ void onSwipeItemFinished2(RecyclerView.ViewHolder holder, int result, int afterReaction) {
+
+ ((SwipeableItemViewHolder) holder).setSwipeResult(result);
+ ((SwipeableItemViewHolder) holder).setAfterSwipeReaction(afterReaction);
+ ((SwipeableItemViewHolder) holder).setSwipeItemSlideAmount(getSwipeAmountFromAfterReaction(result, afterReaction));
+
+ mSwipeableItemAdapter.onPerformAfterSwipeReaction(holder, result, afterReaction);
+ notifyDataSetChanged();
+ }
+
+ protected boolean isSwiping() {
+ return (mSwipingItemPosition != RecyclerView.NO_POSITION);
+ }
+
+ private static float getSwipeAmountFromAfterReaction(int result, int afterReaction) {
+ switch (afterReaction) {
+ case RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT:
+ return 0.0f;
+ case RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION:
+ case RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_REMOVE_ITEM:
+ return (result == RecyclerViewSwipeManager.RESULT_SWIPED_LEFT)
+ ? RecyclerViewSwipeManager.OUTSIDE_OF_THE_WINDOW_LEFT
+ : RecyclerViewSwipeManager.OUTSIDE_OF_THE_WINDOW_RIGHT;
+ default:
+ return 0.0f;
+ }
+ }
+
+ private static void safeUpdateFlags(RecyclerView.ViewHolder holder, int flags) {
+ if (!(holder instanceof SwipeableItemViewHolder)) {
+ return;
+ }
+
+ final SwipeableItemViewHolder holder2 = (SwipeableItemViewHolder) holder;
+
+ final int curFlags = holder2.getSwipeStateFlags();
+ final int mask = ~RecyclerViewSwipeManager.STATE_FLAG_IS_UPDATED;
+
+ // append UPDATED flag
+ if ((curFlags == STATE_FLAG_INITIAL_VALUE) || (((curFlags ^ flags) & mask) != 0)) {
+ flags |= RecyclerViewSwipeManager.STATE_FLAG_IS_UPDATED;
+ }
+
+ ((SwipeableItemViewHolder) holder).setSwipeStateFlags(flags);
+ }
+
+ private static SwipeableItemAdapter getSwipeableItemAdapter(RecyclerView.Adapter adapter) {
+ return WrapperAdapterUtils.findWrappedAdapter(adapter, SwipeableItemAdapter.class);
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipingItemOperator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipingItemOperator.java
new file mode 100644
index 00000000000..c735d436266
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipingItemOperator.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.swipeable;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+class SwipingItemOperator {
+ @SuppressWarnings("unused")
+ private static final String TAG = "SwipingItemOperator";
+
+ private static final int REACTION_CAN_NOT_SWIPE = SwipeReactionUtils.REACTION_CAN_NOT_SWIPE;
+ private static final int REACTION_CAN_NOT_SWIPE_WITH_RUBBER_EFFECT = SwipeReactionUtils.REACTION_CAN_NOT_SWIPE_WITH_RUBBER_EFFECT;
+ private static final int REACTION_CAN_SWIPE = SwipeReactionUtils.REACTION_CAN_SWIPE;
+
+ private static final float RUBBER_BAND_LIMIT = 0.15f;
+
+ private static final Interpolator RUBBER_BAND_INTERPOLATOR = new RubberBandInterpolator(RUBBER_BAND_LIMIT);
+
+ private RecyclerViewSwipeManager mSwipeManager;
+ private RecyclerView.ViewHolder mSwipingItem;
+ private View mSwipingItemContainerView;
+ private int mLeftSwipeReactionType;
+ private int mRightSwipeReactionType;
+ private int mSwipingItemWidth;
+ private float mInvSwipingItemWidth;
+ private int mSwipeDistance;
+ private float mPrevTranslateAmount;
+
+ public SwipingItemOperator(RecyclerViewSwipeManager manager, RecyclerView.ViewHolder swipingItem, int swipeReactionType) {
+ mSwipeManager = manager;
+ mSwipingItem = swipingItem;
+ mLeftSwipeReactionType = SwipeReactionUtils.extractLeftReaction(swipeReactionType);
+ mRightSwipeReactionType = SwipeReactionUtils.extractRightReaction(swipeReactionType);
+
+ mSwipingItemContainerView = ((SwipeableItemViewHolder) swipingItem).getSwipeableContainerView();
+ mSwipingItemWidth = mSwipingItemContainerView.getWidth();
+ mInvSwipingItemWidth = (mSwipingItemWidth != 0) ? (1.0f / mSwipingItemWidth) : 0.0f;
+ }
+
+ @SuppressWarnings("EmptyMethod")
+ public void start() {
+ // nothing to do here.
+ }
+
+ public void finish() {
+ mSwipeManager = null;
+ mSwipingItem = null;
+ mSwipeDistance = 0;
+ mSwipingItemWidth = 0;
+ mInvSwipingItemWidth = 0;
+ mLeftSwipeReactionType = REACTION_CAN_NOT_SWIPE;
+ mRightSwipeReactionType = REACTION_CAN_NOT_SWIPE;
+ mPrevTranslateAmount = 0;
+ mSwipingItemContainerView = null;
+ }
+
+ public void update(int swipeDistance) {
+ if (mSwipeDistance == swipeDistance) {
+ return;
+ }
+
+ mSwipeDistance = swipeDistance;
+
+ final int reactionType = (mSwipeDistance > 0) ? mRightSwipeReactionType : mLeftSwipeReactionType;
+
+ float translateAmount = 0;
+
+ switch (reactionType) {
+ case REACTION_CAN_NOT_SWIPE:
+ break;
+ case REACTION_CAN_NOT_SWIPE_WITH_RUBBER_EFFECT:
+ float proportion = Math.min(Math.abs(mSwipeDistance), mSwipingItemWidth) * mInvSwipingItemWidth;
+ translateAmount = Math.signum(mSwipeDistance) * RUBBER_BAND_INTERPOLATOR.getInterpolation(proportion);
+ break;
+ case REACTION_CAN_SWIPE:
+ translateAmount = Math.min(Math.max((mSwipeDistance * mInvSwipingItemWidth), -1.0f), 1.0f);
+ break;
+ }
+
+ mSwipeManager.applySlideItem(mSwipingItem, mPrevTranslateAmount, translateAmount, false);
+
+ mPrevTranslateAmount = translateAmount;
+ }
+}
\ No newline at end of file
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/touchguard/RecyclerViewTouchActionGuardManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/touchguard/RecyclerViewTouchActionGuardManager.java
new file mode 100644
index 00000000000..3f8003af242
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/touchguard/RecyclerViewTouchActionGuardManager.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.touchguard;
+
+import android.support.v4.view.MotionEventCompat;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+public class RecyclerViewTouchActionGuardManager {
+ private static final String TAG = "ARVTouchActionGuardMgr";
+
+ private static final boolean LOCAL_LOGV = false;
+ private static final boolean LOCAL_LOGD = false;
+
+ private RecyclerView.OnItemTouchListener mInternalUseOnItemTouchListener;
+ private RecyclerView mRecyclerView;
+ private boolean mGuarding;
+ private int mInitialTouchY;
+ private int mLastTouchY;
+ private int mTouchSlop;
+ private boolean mEnabled;
+ private boolean mInterceptScrollingWhileAnimationRunning;
+
+ public RecyclerViewTouchActionGuardManager() {
+ mInternalUseOnItemTouchListener = new RecyclerView.OnItemTouchListener() {
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ return RecyclerViewTouchActionGuardManager.this.onInterceptTouchEvent(rv, e);
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ RecyclerViewTouchActionGuardManager.this.onTouchEvent(rv, e);
+ }
+ };
+ }
+
+ public boolean isReleased() {
+ return (mInternalUseOnItemTouchListener == null);
+ }
+
+ public void attachRecyclerView(RecyclerView rv) {
+ if (rv == null) {
+ throw new IllegalArgumentException("RecyclerView cannot be null");
+ }
+
+ if (isReleased()) {
+ throw new IllegalStateException("Accessing released object");
+ }
+
+ if (mRecyclerView != null) {
+ throw new IllegalStateException("RecyclerView instance has already been set");
+ }
+
+ mRecyclerView = rv;
+ mRecyclerView.addOnItemTouchListener(mInternalUseOnItemTouchListener);
+
+ mTouchSlop = ViewConfiguration.get(rv.getContext()).getScaledTouchSlop();
+ }
+
+ public void release() {
+ if (mRecyclerView != null && mInternalUseOnItemTouchListener != null) {
+ mRecyclerView.removeOnItemTouchListener(mInternalUseOnItemTouchListener);
+ }
+ mInternalUseOnItemTouchListener = null;
+ mRecyclerView = null;
+ }
+
+ /*package*/ boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ if (!mEnabled) {
+ return false;
+ }
+
+ final int action = MotionEventCompat.getActionMasked(e);
+
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "onInterceptTouchEvent() action = " + action);
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ handleActionUpOrCancel();
+ break;
+
+ case MotionEvent.ACTION_DOWN:
+ handleActionDown(e);
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (handleActionMove(rv, e)) {
+ return true;
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ /*package*/ void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ if (!mEnabled) {
+ return;
+ }
+
+ final int action = MotionEventCompat.getActionMasked(e);
+
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "onTouchEvent() action = " + action);
+ }
+
+ switch (action) {
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ handleActionUpOrCancel();
+ break;
+ }
+ }
+
+ private boolean handleActionMove(RecyclerView rv, MotionEvent e) {
+ if (!mGuarding) {
+ mLastTouchY = (int) (e.getY() + 0.5f);
+
+ final int distance = mLastTouchY - mInitialTouchY;
+
+ if (mInterceptScrollingWhileAnimationRunning && (Math.abs(distance) > mTouchSlop) && isAnimationRunning(rv)) {
+ // intercept vertical move touch events while animation is running
+ mGuarding = true;
+ }
+ }
+
+ return mGuarding;
+ }
+
+ private static boolean isAnimationRunning(RecyclerView rv) {
+ final RecyclerView.ItemAnimator itemAnimator = rv.getItemAnimator();
+ return (itemAnimator != null) && (itemAnimator.isRunning());
+ }
+
+ private void handleActionUpOrCancel() {
+ mGuarding = false;
+ mInitialTouchY = 0;
+ mLastTouchY = 0;
+ }
+
+ private void handleActionDown(MotionEvent e) {
+ mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
+ mGuarding = false;
+ }
+
+ public void setEnabled(boolean enabled) {
+ if (mEnabled == enabled) {
+ return;
+ }
+ mEnabled = enabled;
+
+ if (!mEnabled) {
+ handleActionUpOrCancel();
+ }
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public void setInterceptVerticalScrollingWhileAnimationRunning(boolean value) {
+ mInterceptScrollingWhileAnimationRunning = value;
+ }
+
+ public boolean isInterceptScrollingWhileAnimationRunning() {
+ return mInterceptScrollingWhileAnimationRunning;
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableItemViewHolder.java
new file mode 100644
index 00000000000..9cc64c00ae1
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableItemViewHolder.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.utils;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemViewHolder;
+
+public abstract class AbstractDraggableItemViewHolder extends RecyclerView.ViewHolder implements DraggableItemViewHolder {
+ private int mDragStateFlags;
+
+ public AbstractDraggableItemViewHolder(View itemView) {
+ super(itemView);
+ }
+
+ @Override
+ public void setDragStateFlags(int flags) {
+ mDragStateFlags = flags;
+ }
+
+ @Override
+ public int getDragStateFlags() {
+ return mDragStateFlags;
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableSwipeableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableSwipeableItemViewHolder.java
new file mode 100644
index 00000000000..3e8dc254a1a
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableSwipeableItemViewHolder.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.utils;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemViewHolder;
+import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager;
+import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemViewHolder;
+
+public abstract class AbstractDraggableSwipeableItemViewHolder extends RecyclerView.ViewHolder implements DraggableItemViewHolder, SwipeableItemViewHolder {
+ private int mDragStateFlags;
+ private int mSwipeStateFlags;
+ private int mSwipeResult = RecyclerViewSwipeManager.RESULT_NONE;
+ private int mAfterSwipeReaction = RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT;
+ private float mSwipeAmount;
+
+ public AbstractDraggableSwipeableItemViewHolder(View itemView) {
+ super(itemView);
+ }
+
+ @Override
+ public void setDragStateFlags(int flags) {
+ mDragStateFlags = flags;
+ }
+
+ @Override
+ public int getDragStateFlags() {
+ return mDragStateFlags;
+ }
+
+ @Override
+ public void setSwipeStateFlags(int flags) {
+ mSwipeStateFlags = flags;
+ }
+
+ @Override
+ public int getSwipeStateFlags() {
+ return mSwipeStateFlags;
+ }
+
+ @Override
+ public void setSwipeResult(int result) {
+ mSwipeResult = result;
+ }
+
+ @Override
+ public int getSwipeResult() {
+ return mSwipeResult;
+ }
+
+ @Override
+ public int getAfterSwipeReaction() {
+ return mAfterSwipeReaction;
+ }
+
+ @Override
+ public void setAfterSwipeReaction(int reaction) {
+ mAfterSwipeReaction = reaction;
+ }
+
+ @Override
+ public float getSwipeItemSlideAmount() {
+ return mSwipeAmount;
+ }
+
+ @Override
+ public void setSwipeItemSlideAmount(float amount) {
+ mSwipeAmount = amount;
+ }
+
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemAdapter.java
new file mode 100644
index 00000000000..6fb94598a9d
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemAdapter.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.utils;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.ViewGroup;
+
+import com.h6ah4i.android.widget.advrecyclerview.expandable.ExpandableItemAdapter;
+
+public abstract class AbstractExpandableItemAdapter
+ extends RecyclerView.Adapter
+ implements ExpandableItemAdapter {
+
+ /**
+ * This method will not be called.
+ * Override {@link #onCreateGroupViewHolder(android.view.ViewGroup, int)} and
+ * {@link #onCreateChildViewHolder(android.view.ViewGroup, int)} instead.
+ *
+ * @param parent not used
+ * @param viewType not used
+ * @return null
+ */
+ @Override
+ public final RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return null;
+ }
+
+ /**
+ * This method will not be called.
+ * Override {@link #getGroupId(int)} and {@link #getChildId(int, int)} instead.
+ *
+ * @param position not used
+ * @return {@link RecyclerView#NO_ID}
+ */
+ @Override
+ public final long getItemId(int position) {
+ return RecyclerView.NO_ID;
+ }
+
+ /**
+ * This method will not be called.
+ * Override {@link #getGroupItemViewType(int)} and {@link #getChildItemViewType(int, int)} instead.
+ *
+ * @param position not used
+ * @return 0
+ */
+ @Override
+ public final int getItemViewType(int position) {
+ return 0;
+ }
+
+ /**
+ * This method will not be called.
+ * Override {@link #getGroupCount()} and {@link #getChildCount(int)} instead.
+ *
+ * @return 0
+ */
+ @Override
+ public final int getItemCount() {
+ return 0;
+ }
+
+ /**
+ * This method will not be called.
+ * Override {@link #onBindGroupViewHolder(RecyclerView.ViewHolder, int, int)} ()} and
+ * {@link #onBindChildViewHolder(RecyclerView.ViewHolder, int, int, int)} instead.
+ *
+ * @param holder not used
+ * @param position not used
+ */
+ @Override
+ public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ }
+}
\ No newline at end of file
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemViewHolder.java
new file mode 100644
index 00000000000..a0dd61b5aae
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemViewHolder.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.utils;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+import com.h6ah4i.android.widget.advrecyclerview.expandable.ExpandableItemViewHolder;
+
+public abstract class AbstractExpandableItemViewHolder extends RecyclerView.ViewHolder implements ExpandableItemViewHolder {
+ private int mExpandStateFlags;
+
+ public AbstractExpandableItemViewHolder(View itemView) {
+ super(itemView);
+ }
+
+ @Override
+ public void setExpandStateFlags(int flags) {
+ mExpandStateFlags = flags;
+ }
+
+ @Override
+ public int getExpandStateFlags() {
+ return mExpandStateFlags;
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractSwipeableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractSwipeableItemViewHolder.java
new file mode 100644
index 00000000000..f7a084eeb6f
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractSwipeableItemViewHolder.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.utils;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager;
+import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemViewHolder;
+
+public abstract class AbstractSwipeableItemViewHolder extends RecyclerView.ViewHolder implements SwipeableItemViewHolder {
+ private int mSwipeStateFlags;
+ private int mSwipeResult = RecyclerViewSwipeManager.RESULT_NONE;
+ private int mAfterSwipeReaction = RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT;
+ private float mSwipeAmount;
+
+ public AbstractSwipeableItemViewHolder(View itemView) {
+ super(itemView);
+ }
+
+ @Override
+ public void setSwipeStateFlags(int flags) {
+ mSwipeStateFlags = flags;
+ }
+
+ @Override
+ public int getSwipeStateFlags() {
+ return mSwipeStateFlags;
+ }
+
+ @Override
+ public void setSwipeResult(int result) {
+ mSwipeResult = result;
+ }
+
+ @Override
+ public int getSwipeResult() {
+ return mSwipeResult;
+ }
+
+ @Override
+ public int getAfterSwipeReaction() {
+ return mAfterSwipeReaction;
+ }
+
+ @Override
+ public void setAfterSwipeReaction(int reaction) {
+ mAfterSwipeReaction = reaction;
+ }
+
+ @Override
+ public float getSwipeItemSlideAmount() {
+ return mSwipeAmount;
+ }
+
+ @Override
+ public void setSwipeItemSlideAmount(float amount) {
+ mSwipeAmount = amount;
+ }
+
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/BaseWrapperAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/BaseWrapperAdapter.java
new file mode 100644
index 00000000000..a82009ebadd
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/BaseWrapperAdapter.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.utils;
+
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.ViewGroup;
+
+import java.lang.ref.WeakReference;
+
+public class BaseWrapperAdapter
+ extends RecyclerView.Adapter {
+ private static final String TAG = "ARVBaseWrapperAdapter";
+ private static final boolean LOCAL_LOGD = false;
+
+ private RecyclerView.Adapter mWrappedAdapter;
+ private BridgeObserver mBridgeObserver;
+
+ public BaseWrapperAdapter(RecyclerView.Adapter adapter) {
+ mWrappedAdapter = adapter;
+ mBridgeObserver = new BridgeObserver<>(this);
+ mWrappedAdapter.registerAdapterDataObserver(mBridgeObserver);
+ super.setHasStableIds(mWrappedAdapter.hasStableIds());
+ }
+
+ public void release() {
+ onRelease();
+
+ if (mWrappedAdapter != null && mBridgeObserver != null) {
+ mWrappedAdapter.unregisterAdapterDataObserver(mBridgeObserver);
+ }
+
+ mWrappedAdapter = null;
+ mBridgeObserver = null;
+ }
+
+ protected void onRelease() {
+ // override this method if needed
+ }
+
+ public RecyclerView.Adapter getWrappedAdapter() {
+ return mWrappedAdapter;
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+ mWrappedAdapter.onAttachedToRecyclerView(recyclerView);
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
+ mWrappedAdapter.onDetachedFromRecyclerView(recyclerView);
+ }
+
+ @Override
+ public void onViewAttachedToWindow(VH holder) {
+ mWrappedAdapter.onViewAttachedToWindow(holder);
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(VH holder) {
+ mWrappedAdapter.onViewDetachedFromWindow(holder);
+ }
+
+ @Override
+ public void onViewRecycled(VH holder) {
+ mWrappedAdapter.onViewRecycled(holder);
+ }
+
+ @Override
+ public void setHasStableIds(boolean hasStableIds) {
+ super.setHasStableIds(hasStableIds);
+ mWrappedAdapter.setHasStableIds(hasStableIds);
+ }
+
+ @Override
+ public VH onCreateViewHolder(ViewGroup parent, int viewType) {
+ return mWrappedAdapter.onCreateViewHolder(parent, viewType);
+ }
+
+ @Override
+ public void onBindViewHolder(VH holder, int position) {
+ mWrappedAdapter.onBindViewHolder(holder, position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mWrappedAdapter.getItemCount();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mWrappedAdapter.getItemId(position);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return mWrappedAdapter.getItemViewType(position);
+ }
+
+ protected void onHandleWrappedAdapterChanged() {
+ notifyDataSetChanged();
+ }
+
+ protected void onHandleWrappedAdapterItemRangeChanged(int positionStart, int itemCount) {
+ notifyItemRangeChanged(positionStart, itemCount);
+ }
+
+ protected void onHandleWrappedAdapterItemRangeInserted(int positionStart, int itemCount) {
+ notifyItemRangeInserted(positionStart, itemCount);
+ }
+
+ protected void onHandleWrappedAdapterItemRangeRemoved(int positionStart, int itemCount) {
+ notifyItemRangeRemoved(positionStart, itemCount);
+ }
+
+ protected void onHandleWrappedAdapterRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ if (itemCount != 1) {
+ throw new IllegalStateException("itemCount should be always 1 (actual: " + itemCount + ")");
+ }
+
+ notifyItemMoved(fromPosition, toPosition);
+ }
+
+ /*package*/ final void onWrappedAdapterChanged() {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "onWrappedAdapterChanged");
+ }
+
+ onHandleWrappedAdapterChanged();
+ }
+
+ /*package*/ final void onWrappedAdapterItemRangeChanged(int positionStart, int itemCount) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "onWrappedAdapterItemRangeChanged(positionStart = " + positionStart + ", itemCount = " + itemCount + ")");
+ }
+
+ onHandleWrappedAdapterItemRangeChanged(positionStart, itemCount);
+ }
+
+ /*package*/ final void onWrappedAdapterItemRangeInserted(int positionStart, int itemCount) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "onWrappedAdapterItemRangeInserted(positionStart = " + positionStart + ", itemCount = " + itemCount + ")");
+ }
+
+ onHandleWrappedAdapterItemRangeInserted(positionStart, itemCount);
+ }
+
+ /*package*/ final void onWrappedAdapterItemRangeRemoved(int positionStart, int itemCount) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "onWrappedAdapterItemRangeRemoved(positionStart = " + positionStart + ", itemCount = " + itemCount + ")");
+ }
+
+ onHandleWrappedAdapterItemRangeRemoved(positionStart, itemCount);
+ }
+
+ /*package*/ final void onWrappedAdapterRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "onWrappedAdapterRangeMoved(fromPosition = " + fromPosition + ", toPosition = " + toPosition + ", itemCount = " + itemCount + ")");
+ }
+
+ onHandleWrappedAdapterRangeMoved(fromPosition, toPosition, itemCount);
+ }
+
+ private static final class BridgeObserver extends RecyclerView.AdapterDataObserver {
+ private WeakReference> mRefHolder;
+
+ public BridgeObserver(BaseWrapperAdapter holder) {
+ mRefHolder = new WeakReference<>(holder);
+ }
+
+ @Override
+ public void onChanged() {
+ final BaseWrapperAdapter holder = mRefHolder.get();
+ if (holder != null) {
+ holder.onWrappedAdapterChanged();
+ }
+ }
+
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount) {
+ final BaseWrapperAdapter holder = mRefHolder.get();
+ if (holder != null) {
+ holder.onWrappedAdapterItemRangeChanged(positionStart, itemCount);
+ }
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ final BaseWrapperAdapter holder = mRefHolder.get();
+ if (holder != null) {
+ holder.onWrappedAdapterItemRangeInserted(positionStart, itemCount);
+ }
+ }
+
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ final BaseWrapperAdapter holder = mRefHolder.get();
+ if (holder != null) {
+ holder.onWrappedAdapterItemRangeRemoved(positionStart, itemCount);
+ }
+ }
+
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ final BaseWrapperAdapter holder = mRefHolder.get();
+
+ if (holder != null) {
+ holder.onWrappedAdapterRangeMoved(fromPosition, toPosition, itemCount);
+ }
+ }
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/CustomRecyclerViewUtils.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/CustomRecyclerViewUtils.java
new file mode 100644
index 00000000000..f5f96a04313
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/CustomRecyclerViewUtils.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.utils;
+
+import android.graphics.Rect;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class CustomRecyclerViewUtils {
+ public static RecyclerView.ViewHolder findChildViewHolderUnderWithoutTranslation(RecyclerView rv, float x, float y) {
+ final View child = findChildViewUnderWithoutTranslation(rv, x, y);
+ return (child != null) ? rv.getChildViewHolder(child) : null;
+ }
+
+ private static View findChildViewUnderWithoutTranslation(ViewGroup parent, float x, float y) {
+ final int count = parent.getChildCount();
+ for (int i = count - 1; i >= 0; i--) {
+ final View child = parent.getChildAt(i);
+ if (x >= child.getLeft() &&
+ x <= child.getRight() &&
+ y >= child.getTop() &&
+ y <= child.getBottom()) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ public static RecyclerView.ViewHolder findChildViewHolderUnderWithTranslation(RecyclerView rv, float x, float y) {
+ final View child = rv.findChildViewUnder(x, y);
+ return (child != null) ? rv.getChildViewHolder(child) : null;
+ }
+
+ public static Rect getLayoutMargins(View v, Rect outMargins) {
+ final ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
+ if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
+ final ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) layoutParams;
+ outMargins.left = marginLayoutParams.leftMargin;
+ outMargins.right = marginLayoutParams.rightMargin;
+ outMargins.top = marginLayoutParams.topMargin;
+ outMargins.bottom = marginLayoutParams.bottomMargin;
+ } else {
+ outMargins.left = outMargins.right = outMargins.top = outMargins.bottom = 0;
+ }
+ return outMargins;
+ }
+
+ public static Rect getDecorationOffsets(RecyclerView.LayoutManager layoutManager, View view, Rect outDecorations) {
+ outDecorations.left = layoutManager.getLeftDecorationWidth(view);
+ outDecorations.right = layoutManager.getRightDecorationWidth(view);
+ outDecorations.top = layoutManager.getTopDecorationHeight(view);
+ outDecorations.bottom = layoutManager.getBottomDecorationHeight(view);
+
+ return outDecorations;
+ }
+
+ public static Rect getViewBounds(View v, Rect outBounds) {
+ outBounds.left = v.getLeft();
+ outBounds.right = v.getRight();
+ outBounds.top = v.getTop();
+ outBounds.bottom = v.getBottom();
+ return outBounds;
+ }
+
+
+ public static int findFirstVisibleItemPosition(RecyclerView rv) {
+ RecyclerView.LayoutManager layoutManager = rv.getLayoutManager();
+
+ if (layoutManager instanceof LinearLayoutManager) {
+ return (((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition());
+ } else {
+ return RecyclerView.NO_POSITION;
+ }
+ }
+
+ public static int findLastVisibleItemPosition(RecyclerView rv) {
+ RecyclerView.LayoutManager layoutManager = rv.getLayoutManager();
+
+ if (layoutManager instanceof LinearLayoutManager) {
+ return (((LinearLayoutManager) layoutManager).findLastVisibleItemPosition());
+ } else {
+ return RecyclerView.NO_POSITION;
+ }
+ }
+
+ public static int findFirstCompletelyVisibleItemPosition(RecyclerView rv) {
+ RecyclerView.LayoutManager layoutManager = rv.getLayoutManager();
+
+ if (layoutManager instanceof LinearLayoutManager) {
+ return (((LinearLayoutManager) layoutManager).findFirstCompletelyVisibleItemPosition());
+ } else {
+ return RecyclerView.NO_POSITION;
+ }
+ }
+
+ public static int findLastCompletelyVisibleItemPosition(RecyclerView rv) {
+ RecyclerView.LayoutManager layoutManager = rv.getLayoutManager();
+
+ if (layoutManager instanceof LinearLayoutManager) {
+ return (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition());
+ } else {
+ return RecyclerView.NO_POSITION;
+ }
+ }
+}
diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/WrapperAdapterUtils.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/WrapperAdapterUtils.java
new file mode 100644
index 00000000000..c13450cc8bd
--- /dev/null
+++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/WrapperAdapterUtils.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2015 Haruki Hasegawa
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.h6ah4i.android.widget.advrecyclerview.utils;
+
+import android.support.v7.widget.RecyclerView;
+
+public class WrapperAdapterUtils {
+
+ private WrapperAdapterUtils() {
+ }
+
+ public static T findWrappedAdapter(RecyclerView.Adapter adapter, Class clazz) {
+ if (clazz.isInstance(adapter)) {
+ return clazz.cast(adapter);
+ } else if (adapter instanceof BaseWrapperAdapter) {
+ final RecyclerView.Adapter wrappedAdapter = ((BaseWrapperAdapter) adapter).getWrappedAdapter();
+ return findWrappedAdapter(wrappedAdapter, clazz);
+ } else {
+ return null;
+ }
+ }
+
+ public static RecyclerView.Adapter releaseAll(RecyclerView.Adapter adapter) {
+ return releaseCyclically(adapter);
+ }
+
+ private static RecyclerView.Adapter releaseCyclically(RecyclerView.Adapter adapter) {
+ if (!(adapter instanceof BaseWrapperAdapter)) {
+ return adapter;
+ }
+
+ final BaseWrapperAdapter wrapperAdapter = (BaseWrapperAdapter) adapter;
+ final RecyclerView.Adapter wrappedAdapter = wrapperAdapter.getWrappedAdapter();
+
+ wrapperAdapter.release();
+
+ return releaseCyclically(wrappedAdapter);
+ }
+}
diff --git a/mobile/google-android-support-v7-appcompat_lib/project.properties b/mobile/google-android-support-v7-appcompat_lib/project.properties
index 93c8c3c08dd..362a0a3082a 100644
--- a/mobile/google-android-support-v7-appcompat_lib/project.properties
+++ b/mobile/google-android-support-v7-appcompat_lib/project.properties
@@ -11,5 +11,5 @@
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
-target=android-21
+target=android-22
android.library=true
diff --git a/mobile/google-android-support-v7-recyclerview_lib/project.properties b/mobile/google-android-support-v7-recyclerview_lib/project.properties
index 93c8c3c08dd..362a0a3082a 100644
--- a/mobile/google-android-support-v7-recyclerview_lib/project.properties
+++ b/mobile/google-android-support-v7-recyclerview_lib/project.properties
@@ -11,5 +11,5 @@
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
-target=android-21
+target=android-22
android.library=true