From 1b7d60561b92ca10c85e9e2c346fdfd2a3ce39ee Mon Sep 17 00:00:00 2001 From: Doug Keen Date: Wed, 3 Oct 2012 13:07:19 -0700 Subject: [PATCH] Implemented swipe to dismiss for Your Train card --- .../activities/ViewDeparturesActivity.java | 41 ++- .../controls/SwipeDismissTouchListener.java | 265 ++++++++++++++++++ 2 files changed, 295 insertions(+), 11 deletions(-) create mode 100644 src/com/dougkeen/bart/controls/SwipeDismissTouchListener.java diff --git a/src/com/dougkeen/bart/activities/ViewDeparturesActivity.java b/src/com/dougkeen/bart/activities/ViewDeparturesActivity.java index 2cef22a..25aca6c 100644 --- a/src/com/dougkeen/bart/activities/ViewDeparturesActivity.java +++ b/src/com/dougkeen/bart/activities/ViewDeparturesActivity.java @@ -39,6 +39,7 @@ import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; import com.dougkeen.bart.BartRunnerApplication; import com.dougkeen.bart.R; +import com.dougkeen.bart.controls.SwipeDismissTouchListener; import com.dougkeen.bart.controls.Ticker; import com.dougkeen.bart.controls.YourTrainLayout; import com.dougkeen.bart.data.DepartureArrayAdapter; @@ -186,8 +187,21 @@ public class ViewDeparturesActivity extends SActivity implements mMissingDepartureText = findViewById(R.id.missingDepartureText); mMissingDepartureText.setVisibility(View.VISIBLE); - findViewById(R.id.yourTrainSection).setOnClickListener( - mYourTrainSectionClickListener); + mYourTrainSection = (YourTrainLayout) findViewById(R.id.yourTrainSection); + mYourTrainSection.setOnClickListener(mYourTrainSectionClickListener); + mYourTrainSection.setOnTouchListener(new SwipeDismissTouchListener( + mYourTrainSection, null, + new SwipeDismissTouchListener.OnDismissCallback() { + @Override + public void onDismiss(View view, Object token) { + dismissYourTrainSelection(); + getListView().clearChoices(); + getListView().requestLayout(); + if (isYourTrainActionModeActive()) { + mActionMode.finish(); + } + } + })); refreshBoardedDeparture(); @@ -347,6 +361,8 @@ public class ViewDeparturesActivity extends SActivity implements private View mMissingDepartureText; + private YourTrainLayout mYourTrainSection; + protected DepartureArrayAdapter getListAdapter() { return mDeparturesAdapter; } @@ -448,8 +464,7 @@ public class ViewDeparturesActivity extends SActivity implements private void refreshBoardedDeparture() { final Departure boardedDeparture = ((BartRunnerApplication) getApplication()) .getBoardedDeparture(); - final YourTrainLayout yourTrainSection = (YourTrainLayout) findViewById(R.id.yourTrainSection); - int currentVisibility = yourTrainSection.getVisibility(); + int currentVisibility = mYourTrainSection.getVisibility(); final boolean boardedDepartureDoesNotApply = boardedDeparture == null || boardedDeparture.getStationPair() == null @@ -462,10 +477,10 @@ public class ViewDeparturesActivity extends SActivity implements return; } - yourTrainSection.updateFromDeparture(boardedDeparture); + mYourTrainSection.updateFromDeparture(boardedDeparture); if (currentVisibility != View.VISIBLE) { - showYourTrainSection(yourTrainSection); + showYourTrainSection(mYourTrainSection); } if (mActionMode == null) { @@ -656,11 +671,7 @@ public class ViewDeparturesActivity extends SActivity implements startService(intent); return true; } else if (itemId == R.id.delete) { - Intent intent = new Intent(ViewDeparturesActivity.this, - BoardedDepartureService.class); - intent.putExtra("clearBoardedDeparture", true); - startService(intent); - hideYourTrainSection(); + dismissYourTrainSelection(); mode.finish(); return true; } @@ -690,6 +701,14 @@ public class ViewDeparturesActivity extends SActivity implements } } + private void dismissYourTrainSelection() { + Intent intent = new Intent(ViewDeparturesActivity.this, + BoardedDepartureService.class); + intent.putExtra("clearBoardedDeparture", true); + startService(intent); + hideYourTrainSection(); + } + @Override public void onETDChanged(final List departures) { runOnUiThread(new Runnable() { diff --git a/src/com/dougkeen/bart/controls/SwipeDismissTouchListener.java b/src/com/dougkeen/bart/controls/SwipeDismissTouchListener.java new file mode 100644 index 0000000..98e3eda --- /dev/null +++ b/src/com/dougkeen/bart/controls/SwipeDismissTouchListener.java @@ -0,0 +1,265 @@ +/* + * Copyright 2012 Roman Nurik + * + * 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. + * + * ADAPTED FROM https://github.com/JakeWharton/SwipeToDismissNOA + */ + +package com.dougkeen.bart.controls; + +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import com.nineoldandroids.animation.Animator; +import com.nineoldandroids.animation.AnimatorListenerAdapter; +import com.nineoldandroids.animation.ValueAnimator; + +import static com.nineoldandroids.view.ViewHelper.setAlpha; +import static com.nineoldandroids.view.ViewHelper.setTranslationX; +import static com.nineoldandroids.view.ViewPropertyAnimator.animate; + +/** + * A {@link android.view.View.OnTouchListener} that makes any {@link View} + * dismissable when the user swipes (drags her finger) horizontally across the + * view. + * + *

+ * For {@link android.widget.ListView} list items that don't manage their own touch events + * (i.e. you're using + * {@link android.widget.ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener)} + * or an equivalent listener on {@link android.app.ListActivity} or + * {@link android.app.ListFragment}, use {@link SwipeDismissListViewTouchListener} instead. + *

+ * + *

+ * Example usage: + *

+ * + *
+ * view.setOnTouchListener(new SwipeDismissTouchListener(view, null, // Optional
+ * 																	// token/cookie
+ * 																	// object
+ * 		new SwipeDismissTouchListener.OnDismissCallback() {
+ * 			public void onDismiss(View view, Object token) {
+ * 				parent.removeView(view);
+ * 			}
+ * 		}));
+ * 
+ * + *

+ * This class Requires API level 12 or later due to use of + * {@link android.view.ViewPropertyAnimator}. + *

+ * + * @see SwipeDismissListViewTouchListener + */ +public class SwipeDismissTouchListener implements View.OnTouchListener { + // Cached ViewConfiguration and system-wide constant values + private int mSlop; + private int mMinFlingVelocity; + private int mMaxFlingVelocity; + private long mAnimationTime; + + // Fixed properties + private View mView; + private OnDismissCallback mCallback; + private int mViewWidth = 1; // 1 and not 0 to prevent dividing by zero + + // Transient properties + private float mDownX; + private boolean mSwiping; + private Object mToken; + private VelocityTracker mVelocityTracker; + private float mTranslationX; + + /** + * The callback interface used by {@link SwipeDismissTouchListener} to + * inform its client about a successful dismissal of the view for which it + * was created. + */ + public interface OnDismissCallback { + /** + * Called when the user has indicated they she would like to dismiss the + * view. + * + * @param view + * The originating {@link View} to be dismissed. + * @param token + * The optional token passed to this object's constructor. + */ + void onDismiss(View view, Object token); + } + + /** + * Constructs a new swipe-to-dismiss touch listener for the given view. + * + * @param view + * The view to make dismissable. + * @param token + * An optional token/cookie object to be passed through to the + * callback. + * @param callback + * The callback to trigger when the user has indicated that she + * would like to dismiss this view. + */ + public SwipeDismissTouchListener(View view, Object token, + OnDismissCallback callback) { + ViewConfiguration vc = ViewConfiguration.get(view.getContext()); + mSlop = vc.getScaledTouchSlop(); + mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); + mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); + mAnimationTime = view.getContext().getResources() + .getInteger(android.R.integer.config_shortAnimTime); + mView = view; + mToken = token; + mCallback = callback; + } + + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + // offset because the view is translated during swipe + motionEvent.offsetLocation(mTranslationX, 0); + + if (mViewWidth < 2) { + mViewWidth = mView.getWidth(); + } + + switch (motionEvent.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + // TODO: ensure this is a finger, and set a flag + mDownX = motionEvent.getRawX(); + mVelocityTracker = VelocityTracker.obtain(); + mVelocityTracker.addMovement(motionEvent); + view.onTouchEvent(motionEvent); + return false; + } + + case MotionEvent.ACTION_UP: { + if (mVelocityTracker == null) { + break; + } + + float deltaX = motionEvent.getRawX() - mDownX; + mVelocityTracker.addMovement(motionEvent); + 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) { + dismiss = true; + dismissRight = deltaX > 0; + } else if (mMinFlingVelocity <= velocityX + && velocityX <= mMaxFlingVelocity && velocityY < velocityX) { + dismiss = true; + dismissRight = mVelocityTracker.getXVelocity() > 0; + } + if (dismiss) { + // dismiss + animate(mView) + .translationX(dismissRight ? mViewWidth : -mViewWidth) + .alpha(0).setDuration(mAnimationTime) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + performDismiss(); + } + }); + } else { + // cancel + animate(mView).translationX(0).alpha(1) + .setDuration(mAnimationTime).setListener(null); + } + mVelocityTracker = null; + mTranslationX = 0; + mDownX = 0; + mSwiping = false; + break; + } + + case MotionEvent.ACTION_MOVE: { + if (mVelocityTracker == null) { + break; + } + + mVelocityTracker.addMovement(motionEvent); + float deltaX = motionEvent.getRawX() - mDownX; + if (Math.abs(deltaX) > mSlop) { + mSwiping = true; + mView.getParent().requestDisallowInterceptTouchEvent(true); + + // Cancel listview's touch + MotionEvent cancelEvent = MotionEvent.obtain(motionEvent); + cancelEvent + .setAction(MotionEvent.ACTION_CANCEL + | (motionEvent.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); + mView.onTouchEvent(cancelEvent); + } + + if (mSwiping) { + mTranslationX = deltaX; + setTranslationX(mView, deltaX); + // TODO: use an ease-out interpolator or such + setAlpha( + mView, + Math.max( + 0f, + Math.min(1f, 1f - 2f * Math.abs(deltaX) + / mViewWidth))); + return true; + } + break; + } + } + return false; + } + + private void performDismiss() { + // Animate the dismissed view to zero-height and then fire the dismiss + // callback. + // This triggers layout on each animation frame; in the future we may + // want to do something + // smarter and more performant. + + final ViewGroup.LayoutParams lp = mView.getLayoutParams(); + final int originalHeight = mView.getHeight(); + + ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 1) + .setDuration(mAnimationTime); + + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCallback.onDismiss(mView, mToken); + // Reset view presentation + setAlpha(mView, 1f); + setTranslationX(mView, 0); + lp.height = originalHeight; + mView.setLayoutParams(lp); + } + }); + + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + lp.height = (Integer) valueAnimator.getAnimatedValue(); + mView.setLayoutParams(lp); + } + }); + + animator.start(); + } +} \ No newline at end of file