Add bounce effect to contacts list
This commit is contained in:
parent
7e51385dc0
commit
99a4dc254d
3 changed files with 363 additions and 5 deletions
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
Loading…
Reference in a new issue