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