diff --git a/build.gradle b/build.gradle index 0d05d74c3..b8cc47e38 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ ext { dependencies { implementation project(':libs:MemorizingTrustManager') + implementation project(':libs:EnhancedListView') playstoreImplementation 'com.google.android.gms:play-services-gcm:11.8.0' implementation 'org.sufficientlysecure:openpgp-api:10.0' implementation 'com.soundcloud.android:android-crop:1.0.1@aar' @@ -47,7 +48,6 @@ dependencies { implementation 'com.google.zxing:core:3.2.1' implementation 'com.google.zxing:android-integration:3.2.1' implementation 'de.measite.minidns:minidns-hla:0.2.4' - implementation 'de.timroes.android:EnhancedListView:0.3.4' implementation 'me.leolin:ShortcutBadger:1.1.19@aar' implementation 'com.kyleduo.switchbutton:library:1.2.8' implementation 'org.whispersystems:signal-protocol-java:2.6.2' diff --git a/gradle.properties b/gradle.properties index b8a807e22..4a9594aee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1 @@ -org.gradle.jvmargs=-Xmx2048M -android.enableAapt2=false +org.gradle.jvmargs=-Xmx2048M \ No newline at end of file diff --git a/libs/EnhancedListView/build.gradle b/libs/EnhancedListView/build.gradle new file mode 100644 index 000000000..3e5a9e176 --- /dev/null +++ b/libs/EnhancedListView/build.gradle @@ -0,0 +1,98 @@ +apply plugin: 'com.android.library' + +repositories { + mavenCentral() + google() +} + +dependencies { + compile 'com.android.support:support-v4:27.0.2' + compile 'com.nineoldandroids:library:2.4.0' +} + +android { + compileSdkVersion 26 + buildToolsVersion "26.0.2" + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 25 + versionName "0.3.4" + versionCode 9 + } + + lintOptions { + abortOnError false + } +} + +apply plugin: 'maven' +apply plugin: 'signing' + +version = android.defaultConfig.versionName +group = "de.timroes.android" + +if(project.hasProperty("EnhancedListView.properties") && new File(project.property("EnhancedListView.properties")).exists()) { + + Properties props = new Properties() + props.load(new FileInputStream(file(project.property("EnhancedListView.properties")))) + + gradle.taskGraph.whenReady { taskGraph -> + if (taskGraph.allTasks.any { it instanceof Sign }) { + allprojects { ext."signing.keyId" = props['signing.keyId'] } + allprojects { ext."signing.secretKeyRingFile" = props['signing.secretKeyRingFile'] } + allprojects { ext."signing.password" = props['signing.password'] } + } + } + + signing { + required { has("release") && gradle.taskGraph.hasTask("uploadArchives") } + sign configurations.archives + } + + uploadArchives { + + configuration = configurations.archives + repositories.mavenDeployer { + + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + + repository(url: props['sonatypeRepo']) { + authentication(userName: props['sonatypeUsername'], password: props['sonatypePassword']) + } + + pom.project { + + name 'EnhancedListView' + packaging 'aar' + description 'ListView with enhanced features for Android' + url 'https://github.com/timroes/EnhancedListView' + + scm { + url 'scm:git@github.com:timroes/EnhancedListView.git' + connection 'scm:git@github.com:timroes/EnhancedListView.git' + developerConnection 'scm:git@github.com:timroes/EnhancedListView.git' + } + + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + + developers { + developer { + id 'timroes' + name 'Tim Roes' + email 'mail@timroes.de' + } + } + + } + + } + } + +} diff --git a/libs/EnhancedListView/src/main/AndroidManifest.xml b/libs/EnhancedListView/src/main/AndroidManifest.xml new file mode 100644 index 000000000..5f9f17429 --- /dev/null +++ b/libs/EnhancedListView/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/libs/EnhancedListView/src/main/java/de/timroes/android/listview/EnhancedListView.java b/libs/EnhancedListView/src/main/java/de/timroes/android/listview/EnhancedListView.java new file mode 100644 index 000000000..45222f968 --- /dev/null +++ b/libs/EnhancedListView/src/main/java/de/timroes/android/listview/EnhancedListView.java @@ -0,0 +1,969 @@ +/* + * Copyright 2012 - 2013 Roman Nurik, Jake Wharton, Tim Roes + * + * 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 de.timroes.android.listview; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.AbsListView; +import android.widget.Button; +import android.widget.ListView; +import android.widget.PopupWindow; +import android.widget.TextView; + +import com.nineoldandroids.animation.Animator; +import com.nineoldandroids.animation.AnimatorListenerAdapter; +import com.nineoldandroids.animation.ValueAnimator; +import com.nineoldandroids.view.ViewHelper; +import com.nineoldandroids.view.ViewPropertyAnimator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * A {@link android.widget.ListView} offering enhanced features like Swipe To Dismiss and an + * undo functionality. See the documentation on GitHub for more information. + * + * @author Tim Roes + */ +public class EnhancedListView extends ListView { + + /** + * Defines the style in which undos should be displayed and handled in the list. + * Pass this to {@link #setUndoStyle(de.timroes.android.listview.EnhancedListView.UndoStyle)} + * to change the default behavior from {@link #SINGLE_POPUP}. + */ + public enum UndoStyle { + + /** + * Shows a popup window, that allows the user to undo the last + * dismiss. If another element is deleted, the undo popup will undo that deletion. + * The user is only able to undo the last deletion. + */ + SINGLE_POPUP, + + /** + * Shows a popup window, that allows the user to undo the last dismiss. + * If another item is deleted, this will be added to the chain of undos. So pressing + * undo will undo the last deletion, pressing it again will undo the deletion before that, + * and so on. As soon as the popup vanished (e.g. because {@link #setUndoHideDelay(int) autoHideDelay} + * is over) all saved undos will be discarded. + */ + MULTILEVEL_POPUP, + + /** + * Shows a popup window, that allows the user to undo the last dismisses. + * If another item is deleted, while there is still an undo popup visible, the label + * of the button changes to Undo all and a press on the button, will discard + * all stored undos. As soon as the popup vanished (e.g. because {@link #setUndoHideDelay(int) autoHideDelay} + * is over) all saved undos will be discarded. + */ + COLLAPSED_POPUP + + } + + /** + * Defines the direction in which list items can be swiped out to delete them. + * Use {@link #setSwipeDirection(de.timroes.android.listview.EnhancedListView.SwipeDirection)} + * to change the default behavior. + *

+ * Note: This method requires the Swipe to Dismiss feature enabled. Use + * {@link #enableSwipeToDismiss()} + * to enable the feature. + */ + public enum SwipeDirection { + + /** + * The user can swipe each item into both directions (left and right) to delete it. + */ + BOTH, + + /** + * The user can only swipe the items to the beginning of the item to + * delete it. The start of an item is in Left-To-Right languages the left + * side and in Right-To-Left languages the right side. Before API level + * 17 this is always the left side. + */ + START, + + /** + * The user can only swipe the items to the end of the item to delete it. + * This is in Left-To-Right languages the right side in Right-To-Left + * languages the left side. Before API level 17 this will always be the + * right side. + */ + END + + } + + /** + * The callback interface used by {@link #setShouldSwipeCallback(EnhancedListView.OnShouldSwipeCallback)} + * to inform its client that a list item is going to be swiped and check whether is + * should or not. Implement this to prevent some items from be swiped. + */ + public interface OnShouldSwipeCallback { + + /** + * Called when the user is swiping an item from the list. + *

+ * If the user should get the possibility to swipe the item, return true. + * Otherwise, return false to disable swiping for this item. + * + * @param listView The {@link EnhancedListView} the item is wiping from. + * @param position The position of the item to swipe in your adapter. + * @return Whether the item should be swiped or not. + */ + boolean onShouldSwipe(EnhancedListView listView, int position); + + } + + /** + * The callback interface used by {@link #setDismissCallback(EnhancedListView.OnDismissCallback)} + * to inform its client about a successful dismissal of one or more list item positions. + * Implement this to remove items from your adapter, that has been swiped from the list. + */ + public interface OnDismissCallback { + + /** + * Called when the user has deleted an item from the list. The item has been deleted from + * the {@code listView} at {@code position}. Delete this item from your adapter. + *

+ * Don't return from this method, before your item has been deleted from the adapter, meaning + * if you delete the item in another thread, you have to make sure, you don't return from + * this method, before the item has been deleted. Since the way how you delete your item + * depends on your data and adapter, the {@link de.timroes.android.listview.EnhancedListView} + * cannot handle that synchronizing for you. If you return from this method before you removed + * the view from the adapter, you will most likely get errors like exceptions and flashing + * items in the list. + *

+ * If the user should get the possibility to undo this deletion, return an implementation + * of {@link de.timroes.android.listview.EnhancedListView.Undoable} from this method. + * If you return {@code null} no undo will be possible. You are free to return an {@code Undoable} + * for some items, and {@code null} for others, though it might be a horrible user experience. + * + * @param listView The {@link EnhancedListView} the item has been deleted from. + * @param position The position of the item to delete from your adapter. + * @return An {@link de.timroes.android.listview.EnhancedListView.Undoable}, if you want + * to give the user the possibility to undo the deletion. + */ + Undoable onDismiss(EnhancedListView listView, int position); + + } + + /** + * Extend this abstract class and return it from + * {@link EnhancedListView.OnDismissCallback#onDismiss(EnhancedListView, int)} + * to let the user undo the deletion you've done with your {@link EnhancedListView.OnDismissCallback}. + * You have at least to implement the {@link #undo()} method, and can override {@link #discard()} + * and {@link #getTitle()} to offer more functionality. See the README file for example implementations. + */ + public abstract static class Undoable { + + /** + * This method must undo the deletion you've done in + * {@link EnhancedListView.OnDismissCallback#onDismiss(EnhancedListView, int)} and reinsert + * the element into the adapter. + *

+ * In the most implementations, you will only remove the list item from your adapter + * in the {@code onDismiss} method and delete it from the database (or your permanent + * storage) in {@link #discard()}. In that case you only need to reinsert the item + * to the adapter. + */ + public abstract void undo(); + + /** + * Returns the individual undo message for this undo. This will be displayed in the undo + * window, beside the undo button. The default implementation returns {@code null}, + * what will lead in a default message to be displayed in the undo window. + * Don't call the super method, when overriding this method. + * + * @return The title for a special string. + */ + public String getTitle() { + return null; + } + + /** + * Discard the undo, meaning the user has no longer the possibility to undo the deletion. + * Implement this, to finally delete your stuff from permanent storages like databases + * (whereas in {@link de.timroes.android.listview.EnhancedListView.OnDismissCallback#onKeyDown(int, android.view.KeyEvent)} + * you should only remove it from the list adapter). + */ + public void discard() { } + + } + + private class PendingDismissData implements Comparable { + + public int position; + /** + * The view that should get swiped out. + */ + public View view; + /** + * The whole list item view. + */ + public View childView; + + PendingDismissData(int position, View view, View childView) { + this.position = position; + this.view = view; + this.childView = childView; + } + + @Override + public int compareTo(PendingDismissData other) { + // Sort by descending position + return other.position - position; + } + + } + + private class UndoClickListener implements OnClickListener { + + /** + * Called when a view has been clicked. + * + * @param v The view that was clicked. + */ + @Override + public void onClick(View v) { + if(!mUndoActions.isEmpty()) { + switch(mUndoStyle) { + case SINGLE_POPUP: + mUndoActions.get(0).undo(); + mUndoActions.clear(); + break; + case COLLAPSED_POPUP: + Collections.reverse(mUndoActions); + for(Undoable undo : mUndoActions) { + undo.undo(); + } + mUndoActions.clear(); + break; + case MULTILEVEL_POPUP: + mUndoActions.get(mUndoActions.size() - 1).undo(); + mUndoActions.remove(mUndoActions.size() - 1); + break; + } + } + + // Dismiss dialog or change text + if(mUndoActions.isEmpty()) { + if(mUndoPopup.isShowing()) { + mUndoPopup.dismiss(); + } + } else { + changePopupText(); + changeButtonLabel(); + } + + mValidDelayedMsgId++; + } + } + + private class HideUndoPopupHandler extends Handler { + + /** + * Subclasses must implement this to receive messages. + */ + @Override + public void handleMessage(Message msg) { + if(msg.what == mValidDelayedMsgId) { + discardUndo(); + } + } + } + + // Cached ViewConfiguration and system-wide constant values + private float mSlop; + private int mMinFlingVelocity; + private int mMaxFlingVelocity; + private long mAnimationTime; + + private final Object[] mAnimationLock = new Object[0]; + + // Swipe-To-Dismiss + private boolean mSwipeEnabled; + private OnDismissCallback mDismissCallback; + private OnShouldSwipeCallback mShouldSwipeCallback; + private UndoStyle mUndoStyle = UndoStyle.SINGLE_POPUP; + private boolean mTouchBeforeAutoHide = true; + private SwipeDirection mSwipeDirection = SwipeDirection.BOTH; + private int mUndoHideDelay = 5000; + private int mSwipingLayout; + + private List mUndoActions = new ArrayList(); + private SortedSet mPendingDismisses = new TreeSet(); + private List mAnimatedViews = new LinkedList(); + private int mDismissAnimationRefCount; + + private boolean mSwipePaused; + private boolean mSwiping; + private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero + private View mSwipeDownView; + private View mSwipeDownChild; + private TextView mUndoPopupTextView; + private VelocityTracker mVelocityTracker; + private float mDownX; + private int mDownPosition; + private float mScreenDensity; + + private PopupWindow mUndoPopup; + private int mValidDelayedMsgId; + private Handler mHideUndoHandler = new HideUndoPopupHandler(); + private Button mUndoButton; + // END Swipe-To-Dismiss + + /** + * {@inheritDoc} + */ + public EnhancedListView(Context context) { + super(context); + init(context); + } + + /** + * {@inheritDoc} + */ + public EnhancedListView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + /** + * {@inheritDoc} + */ + public EnhancedListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context ctx) { + + if(isInEditMode()) { + // Skip initializing when in edit mode (IDE preview). + return; + } + ViewConfiguration vc =ViewConfiguration.get(ctx); + mSlop = getResources().getDimension(R.dimen.elv_touch_slop); + mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); + mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); + mAnimationTime = ctx.getResources().getInteger( + android.R.integer.config_shortAnimTime); + + // Initialize undo popup + LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View undoView = inflater.inflate(R.layout.elv_undo_popup, null); + mUndoButton = (Button)undoView.findViewById(R.id.undo); + mUndoButton.setOnClickListener(new UndoClickListener()); + mUndoButton.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + // If the user touches the screen invalidate the current running delay by incrementing + // the valid message id. So this delay won't hide the undo popup anymore + mValidDelayedMsgId++; + return false; + } + }); + mUndoPopupTextView = (TextView)undoView.findViewById(R.id.text); + + mUndoPopup = new PopupWindow(undoView, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, false); + mUndoPopup.setAnimationStyle(R.style.elv_fade_animation); + + mScreenDensity = getResources().getDisplayMetrics().density; + // END initialize undo popup + + setOnScrollListener(makeScrollListener()); + + } + + /** + * Enables the Swipe to Dismiss feature for this list. This allows users to swipe out + * an list item element to delete it from the list. Every time the user swipes out an element + * {@link de.timroes.android.listview.EnhancedListView.OnDismissCallback#onDismiss(EnhancedListView, int)} + * of the given {@link de.timroes.android.listview.EnhancedListView} will be called. To enable + * undo of the deletion, return an {@link de.timroes.android.listview.EnhancedListView.Undoable} + * from {@link de.timroes.android.listview.EnhancedListView.OnDismissCallback#onDismiss(EnhancedListView, int)}. + * Return {@code null}, if you don't want the undo feature enabled. Read the README file + * or the demo project for more detailed samples. + * + * @return The {@link de.timroes.android.listview.EnhancedListView} + * @throws java.lang.IllegalStateException when you haven't passed an {@link EnhancedListView.OnDismissCallback} + * to {@link #setDismissCallback(EnhancedListView.OnDismissCallback)} before calling this + * method. + */ + public EnhancedListView enableSwipeToDismiss() { + + if(mDismissCallback == null) { + throw new IllegalStateException("You must pass an OnDismissCallback to the list before enabling Swipe to Dismiss."); + } + + mSwipeEnabled = true; + + return this; + } + + /** + * Disables the Swipe to Dismiss feature for this list. + * + * @return This {@link de.timroes.android.listview.EnhancedListView} + */ + public EnhancedListView disableSwipeToDismiss() { + mSwipeEnabled = false; + return this; + } + + /** + * Sets the callback to be called when the user dismissed an item from the list (either by + * swiping it out - with Swipe to Dismiss enabled - or by deleting it with + * {@link #delete(int)}). You must call this, before you call {@link #delete(int)} or + * {@link #enableSwipeToDismiss()} otherwise you will get an {@link java.lang.IllegalStateException}. + * + * @param dismissCallback The callback used to handle dismisses of list items. + * @return This {@link de.timroes.android.listview.EnhancedListView} + */ + public EnhancedListView setDismissCallback(OnDismissCallback dismissCallback) { + mDismissCallback = dismissCallback; + return this; + } + + /** + * Sets the callback to be called when the user is swiping an item from the list. + * + * @param shouldSwipeCallback The callback used to handle swipes of list items. + * @return This {@link de.timroes.android.listview.EnhancedListView} + */ + public EnhancedListView setShouldSwipeCallback(OnShouldSwipeCallback shouldSwipeCallback) { + mShouldSwipeCallback = shouldSwipeCallback; + return this; + } + + /** + * Sets the undo style of this list. See the javadoc of {@link de.timroes.android.listview.EnhancedListView.UndoStyle} + * for a detailed explanation of the different styles. The default style (if you never call this + * method) is {@link de.timroes.android.listview.EnhancedListView.UndoStyle#SINGLE_POPUP}. + * + * @param undoStyle The style of this listview. + * @return This {@link de.timroes.android.listview.EnhancedListView} + */ + public EnhancedListView setUndoStyle(UndoStyle undoStyle) { + mUndoStyle = undoStyle; + return this; + } + + /** + * Sets the time in milliseconds after which the undo popup automatically disappears. + * The countdown will start when the user touches the screen. If you want to start the countdown + * immediately when the popups appears, call {@link #setRequireTouchBeforeDismiss(boolean)} with + * {@code false}. + * + * @param hideDelay The delay in milliseconds. + * @return This {@link de.timroes.android.listview.EnhancedListView} + */ + public EnhancedListView setUndoHideDelay(int hideDelay) { + mUndoHideDelay = hideDelay; + return this; + } + + /** + * Sets whether another touch on the view is required before the popup counts down to dismiss + * the undo popup. By default this is set to {@code true}. + * + * @param touchBeforeDismiss Whether the screen needs to be touched before the countdown starts. + * @return This {@link de.timroes.android.listview.EnhancedListView} + * + * @see #setUndoHideDelay(int) + */ + public EnhancedListView setRequireTouchBeforeDismiss(boolean touchBeforeDismiss) { + mTouchBeforeAutoHide = touchBeforeDismiss; + return this; + } + + /** + * Sets the directions in which a list item can be swiped to delete. + * By default this is set to {@link SwipeDirection#BOTH} so that an item + * can be swiped into both directions. + *

+ * Note: This method requires the Swipe to Dismiss feature enabled. Use + * {@link #enableSwipeToDismiss()} to enable the feature. + * + * @param direction The direction to which the swipe should be limited. + * @return This {@link de.timroes.android.listview.EnhancedListView} + */ + public EnhancedListView setSwipeDirection(SwipeDirection direction) { + mSwipeDirection = direction; + return this; + } + + /** + * Sets the id of the view, that should be moved, when the user swipes an item. + * Only the view with the specified id will move, while all other views in the list item, will + * stay where they are. This might be usefull to have a background behind the view that is swiped + * out, to stay where it is (and maybe explain that the item is going to be deleted). + * If you never call this method (or call it with 0), the whole view will be swiped. Also if there + * is no view in a list item, with the given id, the whole view will be swiped. + *

+ * Note: This method requires the Swipe to Dismiss feature enabled. Use + * {@link #enableSwipeToDismiss()} to enable the feature. + * + * @param swipingLayoutId The id (from R.id) of the view, that should be swiped. + * @return This {@link de.timroes.android.listview.EnhancedListView} + */ + public EnhancedListView setSwipingLayout(int swipingLayoutId) { + mSwipingLayout = swipingLayoutId; + return this; + } + + /** + * Discard all stored undos and hide the undo popup dialog. + * This method must be called in {@link android.app.Activity#onStop()}. Otherwise + * {@link EnhancedListView.Undoable#discard()} might not be called for several items, what might + * break your data consistency. + */ + public void discardUndo() { + for(Undoable undoable : mUndoActions) { + undoable.discard(); + } + mUndoActions.clear(); + if(mUndoPopup.isShowing()) { + mUndoPopup.dismiss(); + } + } + + /** + * Delete the list item at the specified position. This will animate the item sliding out of the + * list and then collapsing until it vanished (same as if the user slides out an item). + *

+ * NOTE: If you are using list headers, be aware, that the position argument must take care of + * them. Meaning 0 references the first list header. So if you want to delete the first list + * item, you have to pass the number of list headers as {@code position}. Most of the times + * that shouldn't be a problem, since you most probably will evaluate the position which should + * be deleted in a way, that respects the list headers. + * + * @param position The position of the item in the list. + * @throws java.lang.IndexOutOfBoundsException when trying to delete an item outside of the list range. + * @throws java.lang.IllegalStateException when this method is called before an {@link EnhancedListView.OnDismissCallback} + * is set via {@link #setDismissCallback(de.timroes.android.listview.EnhancedListView.OnDismissCallback)}. + * */ + public void delete(int position) { + if(mDismissCallback == null) { + throw new IllegalStateException("You must set an OnDismissCallback, before deleting items."); + } + if(position < 0 || position >= getCount()) { + throw new IndexOutOfBoundsException(String.format("Tried to delete item %d. #items in list: %d", position, getCount())); + } + View childView = getChildAt(position - getFirstVisiblePosition()); + View view = null; + if(mSwipingLayout > 0) { + view = childView.findViewById(mSwipingLayout); + } + if(view == null) { + view = childView; + } + slideOutView(view, childView, position, true); + } + + /** + * Slide out a view to the right or left of the list. After the animation has finished, the + * view will be dismissed by calling {@link #performDismiss(android.view.View, android.view.View, int)}. + * + * @param view The view, that should be slided out. + * @param childView The whole view of the list item. + * @param position The item position of the item. + * @param toRightSide Whether it should slide out to the right side. + */ + private void slideOutView(final View view, final View childView, final int position, boolean toRightSide) { + + // Only start new animation, if this view isn't already animated (too fast swiping bug) + synchronized(mAnimationLock) { + if(mAnimatedViews.contains(view)) { + return; + } + ++mDismissAnimationRefCount; + mAnimatedViews.add(view); + } + + ViewPropertyAnimator.animate(view) + .translationX(toRightSide ? mViewWidth : -mViewWidth) + .alpha(0) + .setDuration(mAnimationTime) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + performDismiss(view, childView, position); + } + }); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + + if (!mSwipeEnabled) { + return super.onTouchEvent(ev); + } + + // Send a delayed message to hide popup + if(mTouchBeforeAutoHide && mUndoPopup.isShowing()) { + mHideUndoHandler.sendMessageDelayed(mHideUndoHandler.obtainMessage(mValidDelayedMsgId), mUndoHideDelay); + } + + // Store width of this list for usage of swipe distance detection + if (mViewWidth < 2) { + mViewWidth = getWidth(); + } + + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + if (mSwipePaused) { + return super.onTouchEvent(ev); + } + + // TODO: ensure this is a finger, and set a flag + + // Find the child view that was touched (perform a hit test) + Rect rect = new Rect(); + int childCount = getChildCount(); + int[] listViewCoords = new int[2]; + getLocationOnScreen(listViewCoords); + int x = (int) ev.getRawX() - listViewCoords[0]; + int y = (int) ev.getRawY() - listViewCoords[1]; + View child; + for (int i = getHeaderViewsCount(); i < childCount; i++) { + child = getChildAt(i); + if(child != null) { + child.getHitRect(rect); + if (rect.contains(x, y)) { + // if a specific swiping layout has been giving, use this to swipe. + if(mSwipingLayout > 0) { + View swipingView = child.findViewById(mSwipingLayout); + if(swipingView != null) { + mSwipeDownView = swipingView; + mSwipeDownChild = child; + break; + } + } + // If no swiping layout has been found, swipe the whole child + mSwipeDownView = mSwipeDownChild = child; + break; + } + } + } + + if (mSwipeDownView != null) { + // test if the item should be swiped + int position = getPositionForView(mSwipeDownView) - getHeaderViewsCount(); + if ((mShouldSwipeCallback == null) || + mShouldSwipeCallback.onShouldSwipe(this, position)) { + mDownX = ev.getRawX(); + mDownPosition = position; + + mVelocityTracker = VelocityTracker.obtain(); + mVelocityTracker.addMovement(ev); + } else { + // set back to null to revert swiping + mSwipeDownView = mSwipeDownChild = null; + } + } + super.onTouchEvent(ev); + return true; + } + + case MotionEvent.ACTION_UP: { + if (mVelocityTracker == null) { + break; + } + + float deltaX = ev.getRawX() - mDownX; + mVelocityTracker.addMovement(ev); + mVelocityTracker.computeCurrentVelocity(1000); + float velocityX = Math.abs(mVelocityTracker.getXVelocity()); + float velocityY = Math.abs(mVelocityTracker.getYVelocity()); + boolean dismiss = false; + boolean dismissRight = false; + if (Math.abs(deltaX) > mViewWidth / 2 && mSwiping) { + dismiss = true; + dismissRight = deltaX > 0; + } else if (mMinFlingVelocity <= velocityX && velocityX <= mMaxFlingVelocity + && velocityY < velocityX && mSwiping && isSwipeDirectionValid(mVelocityTracker.getXVelocity()) + && deltaX >= mViewWidth * 0.2f) { + dismiss = true; + dismissRight = mVelocityTracker.getXVelocity() > 0; + } + if (dismiss) { + // dismiss + slideOutView(mSwipeDownView, mSwipeDownChild, mDownPosition, dismissRight); + } else if(mSwiping) { + // Swipe back to regular position + ViewPropertyAnimator.animate(mSwipeDownView) + .translationX(0) + .alpha(1) + .setDuration(mAnimationTime) + .setListener(null); + } + mVelocityTracker = null; + mDownX = 0; + mSwipeDownView = null; + mSwipeDownChild = null; + mDownPosition = AbsListView.INVALID_POSITION; + mSwiping = false; + break; + } + + case MotionEvent.ACTION_MOVE: { + + if (mVelocityTracker == null || mSwipePaused) { + break; + } + + mVelocityTracker.addMovement(ev); + float deltaX = ev.getRawX() - mDownX; + // Only start swipe in correct direction + if(isSwipeDirectionValid(deltaX)) { + ViewParent parent = getParent(); + if(parent != null) { + // If we swipe don't allow parent to intercept touch (e.g. like NavigationDrawer does) + // otherwise swipe would not be working. + parent.requestDisallowInterceptTouchEvent(true); + } + if (Math.abs(deltaX) > mSlop) { + mSwiping = true; + requestDisallowInterceptTouchEvent(true); + + // Cancel ListView's touch (un-highlighting the item) + MotionEvent cancelEvent = MotionEvent.obtain(ev); + cancelEvent.setAction(MotionEvent.ACTION_CANCEL + | (ev.getActionIndex() + << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); + super.onTouchEvent(cancelEvent); + } + } else { + // If we swiped into wrong direction, act like this was the new + // touch down point + mDownX = ev.getRawX(); + deltaX = 0; + } + + if (mSwiping) { + ViewHelper.setTranslationX(mSwipeDownView, deltaX); + ViewHelper.setAlpha(mSwipeDownView, Math.max(0f, Math.min(1f, + 1f - 2f * Math.abs(deltaX) / mViewWidth))); + return true; + } + break; + } + } + return super.onTouchEvent(ev); + } + + /** + * Animate the dismissed list item to zero-height and fire the dismiss callback when + * all dismissed list item animations have completed. + * + * @param dismissView The view that has been slided out. + * @param listItemView The list item view. This is the whole view of the list item, and not just + * the part, that the user swiped. + * @param dismissPosition The position of the view inside the list. + */ + private void performDismiss(final View dismissView, final View listItemView, final int dismissPosition) { + + final ViewGroup.LayoutParams lp = listItemView.getLayoutParams(); + final int originalLayoutHeight = lp.height; + + int originalHeight = listItemView.getHeight(); + ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 1).setDuration(mAnimationTime); + + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + + // Make sure no other animation is running. Remove animation from running list, that just finished + boolean noAnimationLeft; + synchronized(mAnimationLock) { + --mDismissAnimationRefCount; + mAnimatedViews.remove(dismissView); + noAnimationLeft = mDismissAnimationRefCount == 0; + } + + if (noAnimationLeft) { + // No active animations, process all pending dismisses. + + for(PendingDismissData dismiss : mPendingDismisses) { + if(mUndoStyle == UndoStyle.SINGLE_POPUP) { + for(Undoable undoable : mUndoActions) { + undoable.discard(); + } + mUndoActions.clear(); + } + Undoable undoable = mDismissCallback.onDismiss(EnhancedListView.this, dismiss.position); + if(undoable != null) { + mUndoActions.add(undoable); + } + mValidDelayedMsgId++; + } + + if(!mUndoActions.isEmpty()) { + changePopupText(); + changeButtonLabel(); + + // Show undo popup + float yLocationOffset = getResources().getDimension(R.dimen.elv_undo_bottom_offset); + mUndoPopup.setWidth((int)Math.min(mScreenDensity * 400, getWidth() * 0.9f)); + mUndoPopup.showAtLocation(EnhancedListView.this, + Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, + 0, (int) yLocationOffset); + + // Queue the dismiss only if required + if(!mTouchBeforeAutoHide) { + // Send a delayed message to hide popup + mHideUndoHandler.sendMessageDelayed(mHideUndoHandler.obtainMessage(mValidDelayedMsgId), + mUndoHideDelay); + } + } + + ViewGroup.LayoutParams lp; + for (PendingDismissData pendingDismiss : mPendingDismisses) { + ViewHelper.setAlpha(pendingDismiss.view, 1f); + ViewHelper.setTranslationX(pendingDismiss.view, 0); + lp = pendingDismiss.childView.getLayoutParams(); + lp.height = originalLayoutHeight; + pendingDismiss.childView.setLayoutParams(lp); + } + + mPendingDismisses.clear(); + } + } + }); + + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + lp.height = (Integer) valueAnimator.getAnimatedValue(); + listItemView.setLayoutParams(lp); + } + }); + + mPendingDismisses.add(new PendingDismissData(dismissPosition, dismissView, listItemView)); + animator.start(); + } + + /** + * Changes the text of the undo popup. If more then one item can be undone, the number of deleted + * items will be shown. If only one deletion can be undone, the title of this deletion (or a default + * string in case the title is {@code null}) will be shown. + */ + private void changePopupText() { + String msg = null; + if(mUndoActions.size() > 1) { + msg = getResources().getString(R.string.elv_n_items_deleted, mUndoActions.size()); + } else if(mUndoActions.size() >= 1) { + // Set title from single undoable or when no multiple deletion string + // is given + msg = mUndoActions.get(mUndoActions.size() - 1).getTitle(); + + if(msg == null) { + msg = getResources().getString(R.string.elv_item_deleted); + } + } + mUndoPopupTextView.setText(msg); + } + + /** + * Changes the label of the undo button. + */ + private void changeButtonLabel() { + String msg; + if(mUndoActions.size() > 1 && mUndoStyle == UndoStyle.COLLAPSED_POPUP) { + msg = getResources().getString(R.string.elv_undo_all); + } else { + msg = getResources().getString(R.string.elv_undo); + } + mUndoButton.setText(msg); + } + + private OnScrollListener makeScrollListener() { + return new OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + mSwipePaused = scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL; + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + } + }; + } + + /** + * Checks whether the delta of a swipe indicates, that the swipe is in the + * correct direction, regarding the direction set via + * {@link #setSwipeDirection(de.timroes.android.listview.EnhancedListView.SwipeDirection)} + * + * @param deltaX The delta of x coordinate of the swipe. + * @return Whether the delta of a swipe is in the right direction. + */ + private boolean isSwipeDirectionValid(float deltaX) { + + int rtlSign = 1; + // On API level 17 and above, check if we are in a Right-To-Left layout + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if(getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + rtlSign = -1; + } + } + + // Check if swipe has been done in the correct direction + switch(mSwipeDirection) { + default: + case BOTH: + return true; + case START: + return rtlSign * deltaX < 0; + case END: + return rtlSign * deltaX > 0; + } + + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + + /* + * If the container window no longer visiable, + * dismiss visible undo popup window so it won't leak, + * cos the container window will be destroyed before dismissing the popup window. + */ + if(visibility != View.VISIBLE) { + discardUndo(); + } + } +} diff --git a/libs/EnhancedListView/src/main/res/anim/elv_popup_hide.xml b/libs/EnhancedListView/src/main/res/anim/elv_popup_hide.xml new file mode 100644 index 000000000..9fd948e92 --- /dev/null +++ b/libs/EnhancedListView/src/main/res/anim/elv_popup_hide.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/libs/EnhancedListView/src/main/res/anim/elv_popup_show.xml b/libs/EnhancedListView/src/main/res/anim/elv_popup_show.xml new file mode 100644 index 000000000..9749c97d9 --- /dev/null +++ b/libs/EnhancedListView/src/main/res/anim/elv_popup_show.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/libs/EnhancedListView/src/main/res/drawable-hdpi/elv_ic_action_undo.png b/libs/EnhancedListView/src/main/res/drawable-hdpi/elv_ic_action_undo.png new file mode 100644 index 000000000..67c2496f5 Binary files /dev/null and b/libs/EnhancedListView/src/main/res/drawable-hdpi/elv_ic_action_undo.png differ diff --git a/libs/EnhancedListView/src/main/res/drawable-hdpi/elv_toast_frame.9.png b/libs/EnhancedListView/src/main/res/drawable-hdpi/elv_toast_frame.9.png new file mode 100644 index 000000000..1574c0882 Binary files /dev/null and b/libs/EnhancedListView/src/main/res/drawable-hdpi/elv_toast_frame.9.png differ diff --git a/libs/EnhancedListView/src/main/res/drawable-ldpi/elv_ic_action_undo.png b/libs/EnhancedListView/src/main/res/drawable-ldpi/elv_ic_action_undo.png new file mode 100644 index 000000000..4a9714756 Binary files /dev/null and b/libs/EnhancedListView/src/main/res/drawable-ldpi/elv_ic_action_undo.png differ diff --git a/libs/EnhancedListView/src/main/res/drawable-ldpi/elv_toast_frame.9.png b/libs/EnhancedListView/src/main/res/drawable-ldpi/elv_toast_frame.9.png new file mode 100644 index 000000000..6f0f9d2d0 Binary files /dev/null and b/libs/EnhancedListView/src/main/res/drawable-ldpi/elv_toast_frame.9.png differ diff --git a/libs/EnhancedListView/src/main/res/drawable-mdpi/elv_ic_action_undo.png b/libs/EnhancedListView/src/main/res/drawable-mdpi/elv_ic_action_undo.png new file mode 100644 index 000000000..93261188a Binary files /dev/null and b/libs/EnhancedListView/src/main/res/drawable-mdpi/elv_ic_action_undo.png differ diff --git a/libs/EnhancedListView/src/main/res/drawable-mdpi/elv_toast_frame.9.png b/libs/EnhancedListView/src/main/res/drawable-mdpi/elv_toast_frame.9.png new file mode 100644 index 000000000..e1ff0ebe8 Binary files /dev/null and b/libs/EnhancedListView/src/main/res/drawable-mdpi/elv_toast_frame.9.png differ diff --git a/libs/EnhancedListView/src/main/res/drawable-xhdpi/elv_ic_action_undo.png b/libs/EnhancedListView/src/main/res/drawable-xhdpi/elv_ic_action_undo.png new file mode 100644 index 000000000..2b7996036 Binary files /dev/null and b/libs/EnhancedListView/src/main/res/drawable-xhdpi/elv_ic_action_undo.png differ diff --git a/libs/EnhancedListView/src/main/res/drawable-xhdpi/elv_toast_frame.9.png b/libs/EnhancedListView/src/main/res/drawable-xhdpi/elv_toast_frame.9.png new file mode 100644 index 000000000..334a5bde2 Binary files /dev/null and b/libs/EnhancedListView/src/main/res/drawable-xhdpi/elv_toast_frame.9.png differ diff --git a/libs/EnhancedListView/src/main/res/drawable-xxhdpi/elv_ic_action_undo.png b/libs/EnhancedListView/src/main/res/drawable-xxhdpi/elv_ic_action_undo.png new file mode 100644 index 000000000..aafe28670 Binary files /dev/null and b/libs/EnhancedListView/src/main/res/drawable-xxhdpi/elv_ic_action_undo.png differ diff --git a/libs/EnhancedListView/src/main/res/drawable-xxhdpi/elv_toast_frame.9.png b/libs/EnhancedListView/src/main/res/drawable-xxhdpi/elv_toast_frame.9.png new file mode 100644 index 000000000..99efd3fe9 Binary files /dev/null and b/libs/EnhancedListView/src/main/res/drawable-xxhdpi/elv_toast_frame.9.png differ diff --git a/libs/EnhancedListView/src/main/res/drawable/elv_popup_bg.xml b/libs/EnhancedListView/src/main/res/drawable/elv_popup_bg.xml new file mode 100644 index 000000000..4dfc0880e --- /dev/null +++ b/libs/EnhancedListView/src/main/res/drawable/elv_popup_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/libs/EnhancedListView/src/main/res/drawable/elv_undo_btn_bg.xml b/libs/EnhancedListView/src/main/res/drawable/elv_undo_btn_bg.xml new file mode 100644 index 000000000..fde26ad09 --- /dev/null +++ b/libs/EnhancedListView/src/main/res/drawable/elv_undo_btn_bg.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/libs/EnhancedListView/src/main/res/drawable/elv_undo_btn_bg_focused.xml b/libs/EnhancedListView/src/main/res/drawable/elv_undo_btn_bg_focused.xml new file mode 100644 index 000000000..d3c63634a --- /dev/null +++ b/libs/EnhancedListView/src/main/res/drawable/elv_undo_btn_bg_focused.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/libs/EnhancedListView/src/main/res/drawable/elv_undo_btn_bg_pressed.xml b/libs/EnhancedListView/src/main/res/drawable/elv_undo_btn_bg_pressed.xml new file mode 100644 index 000000000..ca09bdb8a --- /dev/null +++ b/libs/EnhancedListView/src/main/res/drawable/elv_undo_btn_bg_pressed.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/libs/EnhancedListView/src/main/res/layout-v19/elv_undo_popup.xml b/libs/EnhancedListView/src/main/res/layout-v19/elv_undo_popup.xml new file mode 100644 index 000000000..23081757d --- /dev/null +++ b/libs/EnhancedListView/src/main/res/layout-v19/elv_undo_popup.xml @@ -0,0 +1,43 @@ + + + + + + + +