From 99a4dc254d9e72eeec917f789ae6b4616789ae7a Mon Sep 17 00:00:00 2001 From: Severiano Jaramillo Date: Tue, 10 Jul 2018 17:43:40 -0500 Subject: [PATCH] Add bounce effect to contacts list --- .../fragments/ContactsFragment.java | 59 +++- .../util/BounceTouchListener.java | 307 ++++++++++++++++++ app/src/main/res/layout/fragment_contacts.xml | 2 +- 3 files changed, 363 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/cy/agorise/crystalwallet/util/BounceTouchListener.java diff --git a/app/src/main/java/cy/agorise/crystalwallet/fragments/ContactsFragment.java b/app/src/main/java/cy/agorise/crystalwallet/fragments/ContactsFragment.java index 54f69cd..e141f91 100644 --- a/app/src/main/java/cy/agorise/crystalwallet/fragments/ContactsFragment.java +++ b/app/src/main/java/cy/agorise/crystalwallet/fragments/ContactsFragment.java @@ -1,15 +1,18 @@ package cy.agorise.crystalwallet.fragments; +import android.app.Activity; import android.arch.lifecycle.LiveData; import android.arch.lifecycle.Observer; import android.arch.lifecycle.ViewModelProviders; import android.arch.paging.PagedList; +import android.graphics.Point; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; +import android.view.Display; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -18,16 +21,21 @@ import butterknife.BindView; import butterknife.ButterKnife; import cy.agorise.crystalwallet.R; import cy.agorise.crystalwallet.models.Contact; +import cy.agorise.crystalwallet.util.BounceTouchListener; import cy.agorise.crystalwallet.viewmodels.ContactListViewModel; import cy.agorise.crystalwallet.views.ContactListAdapter; public class ContactsFragment extends Fragment { - @BindView(R.id.recyclerViewContacts) - RecyclerView recyclerViewContacts; + @BindView(R.id.rvContacts) + RecyclerView rvContacts; ContactListAdapter adapter; + // Fields used to achieve bounce effect while over-scrolling the contacts list + private BounceTouchListener bounceTouchListener; + float pivotY1, pivotY2; + public ContactsFragment() { // Required empty public constructor } @@ -52,9 +60,11 @@ public class ContactsFragment extends Fragment { ButterKnife.bind(this, view); // Configure RecyclerView and its adapter - recyclerViewContacts.setLayoutManager(new LinearLayoutManager(getContext())); + rvContacts.setLayoutManager(new LinearLayoutManager(getContext())); adapter = new ContactListAdapter(); - recyclerViewContacts.setAdapter(adapter); + rvContacts.setAdapter(adapter); + + configureListBounceEffect(); // Gets contacts LiveData instance from ContactsViewModel ContactListViewModel contactListViewModel = @@ -70,4 +80,45 @@ public class ContactsFragment extends Fragment { return view; } + + private void configureListBounceEffect() { + rvContacts.setPivotX(getScreenWidth(getActivity()) * 0.5f); + + pivotY1 = 0; + pivotY2 = (getScreenHeight(getActivity())) * .5f; + + bounceTouchListener = BounceTouchListener.create(rvContacts, new BounceTouchListener.OnTranslateListener() { + @Override + public void onTranslate(float translation) { + if(translation > 0) { + bounceTouchListener.setMaxAbsTranslation(-99); + rvContacts.setPivotY(pivotY1); + float scale = ((2 * translation) / rvContacts.getMeasuredHeight()) + 1; + rvContacts.setScaleY((float) Math.pow(scale, .6f)); + } else { + bounceTouchListener.setMaxAbsTranslation((int) (pivotY2 * .33f)); + rvContacts.setPivotY(pivotY2); + float scale = ((2 * translation) / rvContacts.getMeasuredHeight()) + 1; + rvContacts.setScaleY((float) Math.pow(scale, .5f)); + } + } + }); + + // Sets custom touch listener to handle bounce/stretch effect + rvContacts.setOnTouchListener(bounceTouchListener); + } + + public static int getScreenWidth(Activity activity) { + Display display = activity.getWindowManager().getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + return size.x; + } + + public static int getScreenHeight(Activity activity) { + Display display = activity.getWindowManager().getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + return size.y; + } } diff --git a/app/src/main/java/cy/agorise/crystalwallet/util/BounceTouchListener.java b/app/src/main/java/cy/agorise/crystalwallet/util/BounceTouchListener.java new file mode 100644 index 0000000..4053ad9 --- /dev/null +++ b/app/src/main/java/cy/agorise/crystalwallet/util/BounceTouchListener.java @@ -0,0 +1,307 @@ +package cy.agorise.crystalwallet.util; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.support.annotation.IdRes; +import android.support.annotation.Nullable; +import android.support.v4.view.MotionEventCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.widget.ListView; +import android.widget.ScrollView; + +/** + * Found this class on the internet and did some changes do adjust it to our + * needs but I still need to figure out some stuff to obtain the exact desired + * animation + */ + +public class BounceTouchListener implements View.OnTouchListener { + private static final long DEFAULT_ANIMATION_TIME = 600L; + + private boolean downCalled = false; + private OnTranslateListener onTranslateListener; + private View mMainView; + private View mContent; + private float mDownY; + private boolean mSwipingDown; + private boolean mSwipingUp; + private Interpolator mInterpolator = new DecelerateInterpolator(3f); + private boolean swipeUpEnabled = true; + private int mActivePointerId = -99; + private float mLastTouchY = -99; + private int mMaxAbsTranslation = -99; + + + private BounceTouchListener(View mainView, int contentResId, @Nullable OnTranslateListener listener) { + mMainView = mainView; + mContent = (contentResId == -1) ? mMainView : mMainView.findViewById(contentResId); + onTranslateListener = listener; + } + + /** + * Creates a new BounceTouchListener + * + * @param mainScrollableView The main view that this touch listener is attached to + * @param onTranslateListener To perform action on translation, can be null if not needed + * @return A new BounceTouchListener attached to the given scrollable view + */ + public static BounceTouchListener create(View mainScrollableView, @Nullable OnTranslateListener onTranslateListener) { + return create(mainScrollableView, -1, onTranslateListener); + } + + /** + * Creates a new BounceTouchListener + * + * @param mainView The main view that this touch listener is attached to + * @param contentResId Resource Id of the scrollable view + * @param onTranslateListener To perform action on translation, can be null if not needed + * @return A new BounceTouchListener attached to the given scrollable view + */ + public static BounceTouchListener create(View mainView, @IdRes int contentResId, + @Nullable OnTranslateListener onTranslateListener) { + return new BounceTouchListener(mainView, contentResId, onTranslateListener); + } + + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + final int action = MotionEventCompat.getActionMasked(motionEvent); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + onDownMotionEvent(motionEvent); + view.onTouchEvent(motionEvent); + downCalled = true; + if (mContent.getTranslationY() == 0) { + return false; + } + } + case MotionEvent.ACTION_MOVE: { + if (mActivePointerId == -99) { + onDownMotionEvent(motionEvent); + downCalled = true; + } + final int pointerIndex = + MotionEventCompat.findPointerIndex(motionEvent, mActivePointerId); + final float y = MotionEventCompat.getY(motionEvent, pointerIndex); + + if (!hasHitTop() && !hasHitBottom() || !downCalled) { + if (!downCalled) { + downCalled = true; + } + mDownY = y; + view.onTouchEvent(motionEvent); + return false; + } + + float deltaY = y - mDownY; + if (Math.abs(deltaY) > 0 && hasHitTop() && deltaY > 0) { + mSwipingDown = true; + sendCancelEventToView(view, motionEvent); + } + if (swipeUpEnabled) { + if (Math.abs(deltaY) > 0 && hasHitBottom() && deltaY < 0) { + mSwipingUp = true; + sendCancelEventToView(view, motionEvent); + } + } + if (mSwipingDown || mSwipingUp) { + if ((deltaY <= 0 && mSwipingDown) || (deltaY >= 0 && mSwipingUp)) { + mDownY = 0; + mSwipingDown = false; + mSwipingUp = false; + downCalled = false; + MotionEvent downEvent = MotionEvent.obtain(motionEvent); + downEvent.setAction(MotionEvent.ACTION_DOWN | + (MotionEventCompat.getActionIndex(motionEvent) << MotionEventCompat.ACTION_POINTER_INDEX_SHIFT)); + view.onTouchEvent(downEvent); + break; + } + int translation = (int) ((deltaY / Math.abs(deltaY)) * Math.pow(Math.abs(deltaY), .8f)); + if (mMaxAbsTranslation > 0) { + if (translation < 0) { + translation = Math.max(-mMaxAbsTranslation, translation); + } else { + translation = Math.min(mMaxAbsTranslation, translation); + } + } + mContent.setTranslationY(translation); + if (onTranslateListener != null) + onTranslateListener.onTranslate(mContent.getTranslationY()); + + return true; + } + break; + } + + case MotionEvent.ACTION_UP: { + mActivePointerId = -99; + // cancel + mContent.animate() + .setInterpolator(mInterpolator) + .translationY(0) + .setDuration(DEFAULT_ANIMATION_TIME) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + ((ValueAnimator) animation).addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + if (onTranslateListener != null) { + onTranslateListener.onTranslate(mContent.getTranslationY()); + } + } + }); + super.onAnimationStart(animation); + } + }); + + mDownY = 0; + mSwipingDown = false; + mSwipingUp = false; + downCalled = false; + break; + } + + case MotionEvent.ACTION_CANCEL: { + mActivePointerId = -99; + break; + } + + case MotionEvent.ACTION_POINTER_UP: { + final int pointerIndex = MotionEventCompat.getActionIndex(motionEvent); + final int pointerId = MotionEventCompat.getPointerId(motionEvent, pointerIndex); + + if (pointerId == mActivePointerId) { + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastTouchY = MotionEventCompat.getY(motionEvent, newPointerIndex); + mActivePointerId = MotionEventCompat.getPointerId(motionEvent, newPointerIndex); + + if (mContent.getTranslationY() > 0) { + mDownY = mLastTouchY - (int) Math.pow(mContent.getTranslationY(), 10f / 8f); + mContent.animate().cancel(); + } else if (mContent.getTranslationY() < 0) { + mDownY = mLastTouchY + (int) Math.pow(-mContent.getTranslationY(), 10f / 8f); + mContent.animate().cancel(); + } + } + break; + } + } + return false; + } + + private void sendCancelEventToView(View view, MotionEvent motionEvent) { + ((ViewGroup) view).requestDisallowInterceptTouchEvent(true); + MotionEvent cancelEvent = MotionEvent.obtain(motionEvent); + cancelEvent.setAction(MotionEvent.ACTION_CANCEL | + (MotionEventCompat.getActionIndex(motionEvent) << MotionEventCompat.ACTION_POINTER_INDEX_SHIFT)); + view.onTouchEvent(cancelEvent); + } + + private void onDownMotionEvent(MotionEvent motionEvent) { + final int pointerIndex = MotionEventCompat.getActionIndex(motionEvent); + mLastTouchY = MotionEventCompat.getY(motionEvent, pointerIndex); + mActivePointerId = MotionEventCompat.getPointerId(motionEvent, 0); + + if (mContent.getTranslationY() > 0) { + mDownY = mLastTouchY - (int) Math.pow(mContent.getTranslationY(), 10f / 8f); + mContent.animate().cancel(); + } else if (mContent.getTranslationY() < 0) { + mDownY = mLastTouchY + (int) Math.pow(-mContent.getTranslationY(), 10f / 8f); + mContent.animate().cancel(); + } else { + mDownY = mLastTouchY; + } + } + + private boolean hasHitBottom() { + if (mMainView instanceof ScrollView) { + ScrollView scrollView = (ScrollView) mMainView; + View view = scrollView.getChildAt(scrollView.getChildCount() - 1); + int diff = (view.getBottom() - (scrollView.getHeight() + scrollView.getScrollY()));// Calculate the scrolldiff + return diff == 0; + } else if (mMainView instanceof ListView) { + ListView listView = (ListView) mMainView; + if (listView.getAdapter() != null) { + if (listView.getAdapter().getCount() > 0) { + return listView.getLastVisiblePosition() == listView.getAdapter().getCount() - 1 && + listView.getChildAt(listView.getChildCount() - 1).getBottom() <= listView.getHeight(); + } + } + } else if (mMainView instanceof RecyclerView) { + RecyclerView recyclerView = (RecyclerView) mMainView; + if (recyclerView.getAdapter() != null && recyclerView.getLayoutManager() != null) { + RecyclerView.Adapter adapter = recyclerView.getAdapter(); + if (adapter.getItemCount() > 0) { + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if (layoutManager instanceof LinearLayoutManager) { + LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager; + return linearLayoutManager.findLastCompletelyVisibleItemPosition() == adapter.getItemCount() - 1; + } else if (layoutManager instanceof StaggeredGridLayoutManager) { + StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager; + int[] checks = staggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(null); + for (int check : checks) { + if (check == adapter.getItemCount() - 1) + return true; + } + } + } + } + } + return false; + } + + private boolean hasHitTop() { + if (mMainView instanceof ScrollView) { + ScrollView scrollView = (ScrollView) mMainView; + return scrollView.getScrollY() == 0; + } else if (mMainView instanceof ListView) { + ListView listView = (ListView) mMainView; + if (listView.getAdapter() != null) { + if (listView.getAdapter().getCount() > 0) { + return listView.getFirstVisiblePosition() == 0 && + listView.getChildAt(0).getTop() >= 0; + } + } + } else if (mMainView instanceof RecyclerView) { + RecyclerView recyclerView = (RecyclerView) mMainView; + if (recyclerView.getAdapter() != null && recyclerView.getLayoutManager() != null) { + RecyclerView.Adapter adapter = recyclerView.getAdapter(); + if (adapter.getItemCount() > 0) { + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if (layoutManager instanceof LinearLayoutManager) { + LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager; + return linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0; + } else if (layoutManager instanceof StaggeredGridLayoutManager) { + StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager; + int[] checks = staggeredGridLayoutManager.findFirstCompletelyVisibleItemPositions(null); + for (int check : checks) { + if (check == 0) + return true; + } + } + } + } + } + + return false; + } + + public void setMaxAbsTranslation(int maxAbsTranslation) { + this.mMaxAbsTranslation = maxAbsTranslation; + } + + public interface OnTranslateListener { + void onTranslate(float translation); + } +} + + diff --git a/app/src/main/res/layout/fragment_contacts.xml b/app/src/main/res/layout/fragment_contacts.xml index 043bf37..c2147c1 100644 --- a/app/src/main/res/layout/fragment_contacts.xml +++ b/app/src/main/res/layout/fragment_contacts.xml @@ -5,7 +5,7 @@ tools:context="cy.agorise.crystalwallet.fragments.ContactsFragment">