Add bounce effect to contacts list

This commit is contained in:
Severiano Jaramillo 2018-07-10 17:43:40 -05:00
parent 7e51385dc0
commit 99a4dc254d
3 changed files with 363 additions and 5 deletions

View file

@ -1,15 +1,18 @@
package cy.agorise.crystalwallet.fragments; package cy.agorise.crystalwallet.fragments;
import android.app.Activity;
import android.arch.lifecycle.LiveData; import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.Observer; import android.arch.lifecycle.Observer;
import android.arch.lifecycle.ViewModelProviders; import android.arch.lifecycle.ViewModelProviders;
import android.arch.paging.PagedList; import android.arch.paging.PagedList;
import android.graphics.Point;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.view.Display;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -18,16 +21,21 @@ import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import cy.agorise.crystalwallet.R; import cy.agorise.crystalwallet.R;
import cy.agorise.crystalwallet.models.Contact; import cy.agorise.crystalwallet.models.Contact;
import cy.agorise.crystalwallet.util.BounceTouchListener;
import cy.agorise.crystalwallet.viewmodels.ContactListViewModel; import cy.agorise.crystalwallet.viewmodels.ContactListViewModel;
import cy.agorise.crystalwallet.views.ContactListAdapter; import cy.agorise.crystalwallet.views.ContactListAdapter;
public class ContactsFragment extends Fragment { public class ContactsFragment extends Fragment {
@BindView(R.id.recyclerViewContacts) @BindView(R.id.rvContacts)
RecyclerView recyclerViewContacts; RecyclerView rvContacts;
ContactListAdapter adapter; ContactListAdapter adapter;
// Fields used to achieve bounce effect while over-scrolling the contacts list
private BounceTouchListener bounceTouchListener;
float pivotY1, pivotY2;
public ContactsFragment() { public ContactsFragment() {
// Required empty public constructor // Required empty public constructor
} }
@ -52,9 +60,11 @@ public class ContactsFragment extends Fragment {
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
// Configure RecyclerView and its adapter // Configure RecyclerView and its adapter
recyclerViewContacts.setLayoutManager(new LinearLayoutManager(getContext())); rvContacts.setLayoutManager(new LinearLayoutManager(getContext()));
adapter = new ContactListAdapter(); adapter = new ContactListAdapter();
recyclerViewContacts.setAdapter(adapter); rvContacts.setAdapter(adapter);
configureListBounceEffect();
// Gets contacts LiveData instance from ContactsViewModel // Gets contacts LiveData instance from ContactsViewModel
ContactListViewModel contactListViewModel = ContactListViewModel contactListViewModel =
@ -70,4 +80,45 @@ public class ContactsFragment extends Fragment {
return view; 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;
}
} }

View file

@ -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);
}
}

View file

@ -5,7 +5,7 @@
tools:context="cy.agorise.crystalwallet.fragments.ContactsFragment"> tools:context="cy.agorise.crystalwallet.fragments.ContactsFragment">
<android.support.v7.widget.RecyclerView <android.support.v7.widget.RecyclerView
android:id="@+id/recyclerViewContacts" android:id="@+id/rvContacts"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:listitem="@layout/contact_list_item" /> tools:listitem="@layout/contact_list_item" />