new external project (advrecyclerview) for eclipse - do not use with gradle build

This commit is contained in:
Peter Siegmund
2015-04-24 08:03:43 +02:00
parent 8683a63d79
commit b45da62992
68 changed files with 9977 additions and 6 deletions
+1
View File
@@ -33,3 +33,4 @@ out/
local.properties
git.xml
build.info
mobile/github-advrecyclerview_lib/bin
@@ -11,7 +11,7 @@
#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
@@ -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
@@ -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
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>com.h6ah4i.android.widget.advrecyclerview</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
@@ -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
@@ -0,0 +1,23 @@
<!--
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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.h6ah4i.android.widget.advrecyclerview">
<application>
<!--suppress AndroidDomInspection -->
<activity android:name=".TestActivity"/>
</application>
</manifest>
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
</lint>
@@ -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
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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 +
'}';
}
}
@@ -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<T extends ItemAnimationInfo> {
protected BaseItemAnimator mItemAnimator;
protected List<T> mPending;
protected List<List<T>> mDeferredReadySets;
protected List<RecyclerView.ViewHolder> 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<RecyclerView.ViewHolder> 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<T> 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<T> 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<T> 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);
}
}
}
@@ -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 +
'}';
}
}
@@ -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<AddAnimationInfo> {
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);
}
@@ -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);
}
@@ -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<ChangeAnimationInfo> {
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);
}
@@ -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<MoveAnimationInfo> {
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);
}
@@ -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<RemoveAnimationInfo> {
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);
}
@@ -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 +
'}';
}
}
@@ -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 +
'}';
}
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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;
}
}
@@ -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;
}
}
}
@@ -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<T extends RecyclerView.ViewHolder> {
/**
* 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);
}
@@ -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();
}
@@ -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<VH extends RecyclerView.ViewHolder> extends BaseWrapperAdapter<VH> {
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<VH> 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);
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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 +
'}';
}
}
@@ -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;
}
}
@@ -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<T> {
protected boolean mReleased;
protected RecyclerView mRecyclerView;
protected List<T> 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. (&lt; 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");
}
}
}
@@ -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);
}
@@ -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<RecyclerView.OnScrollListener> {
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<RecyclerViewOnScrollEventDistributor> 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();
}
}
}
@@ -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<RecyclerView.RecyclerListener> {
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<RecyclerViewRecyclerEventDistributor> 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();
}
}
}
@@ -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() {
}
}
@@ -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<GVH extends RecyclerView.ViewHolder, CVH extends RecyclerView.ViewHolder> {
/**
* 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);
}
@@ -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<GVH extends RecyclerView.ViewHolder, CVH extends RecyclerView.ViewHolder> {
/**
* 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);
}
@@ -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();
}
@@ -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);
}
}
@@ -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<RecyclerView.ViewHolder>
implements DraggableItemAdapter<RecyclerView.ViewHolder>,
SwipeableItemAdapter<RecyclerView.ViewHolder> {
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<RecyclerView.ViewHolder> 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);
}
}
}
}
@@ -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<GVH extends RecyclerView.ViewHolder, CVH extends RecyclerView.ViewHolder> {
/**
* 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);
}
@@ -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<SavedState> CREATOR = new Creator<SavedState>() {
public SavedState createFromParcel(Parcel source) {
return new SavedState(source);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}
@@ -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<RecyclerView.ViewHolder> 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;
}
}
@@ -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<RecyclerView.ViewHolder> 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;
}
}
@@ -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<RemovingItemDecorator> 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);
}
}
}
}
@@ -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));
}
}
@@ -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);
}
}
@@ -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<T extends RecyclerView.ViewHolder> {
/**
* 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);
}
@@ -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();
}
@@ -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<VH extends RecyclerView.ViewHolder> extends BaseWrapperAdapter<VH> {
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<VH> 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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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<GVH extends RecyclerView.ViewHolder, CVH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<RecyclerView.ViewHolder>
implements ExpandableItemAdapter<GVH, CVH> {
/**
* 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) {
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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<VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
private static final String TAG = "ARVBaseWrapperAdapter";
private static final boolean LOCAL_LOGD = false;
private RecyclerView.Adapter<VH> mWrappedAdapter;
private BridgeObserver mBridgeObserver;
public BaseWrapperAdapter(RecyclerView.Adapter<VH> 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<VH> 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<VH extends RecyclerView.ViewHolder> extends RecyclerView.AdapterDataObserver {
private WeakReference<BaseWrapperAdapter<VH>> mRefHolder;
public BridgeObserver(BaseWrapperAdapter<VH> holder) {
mRefHolder = new WeakReference<>(holder);
}
@Override
public void onChanged() {
final BaseWrapperAdapter<VH> holder = mRefHolder.get();
if (holder != null) {
holder.onWrappedAdapterChanged();
}
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
final BaseWrapperAdapter<VH> holder = mRefHolder.get();
if (holder != null) {
holder.onWrappedAdapterItemRangeChanged(positionStart, itemCount);
}
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
final BaseWrapperAdapter<VH> holder = mRefHolder.get();
if (holder != null) {
holder.onWrappedAdapterItemRangeInserted(positionStart, itemCount);
}
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
final BaseWrapperAdapter<VH> holder = mRefHolder.get();
if (holder != null) {
holder.onWrappedAdapterItemRangeRemoved(positionStart, itemCount);
}
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
final BaseWrapperAdapter<VH> holder = mRefHolder.get();
if (holder != null) {
holder.onWrappedAdapterRangeMoved(fromPosition, toPosition, itemCount);
}
}
}
}
@@ -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;
}
}
}
@@ -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> T findWrappedAdapter(RecyclerView.Adapter adapter, Class<T> 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);
}
}
@@ -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
@@ -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