Create BounceTouchListener class in the project and use it to add a Bouncy effect to the Transactions items when the user tries to overscroll.

This commit is contained in:
Severiano Jaramillo 2018-12-19 09:43:09 -06:00
parent 5af88291bc
commit cee1753184
3 changed files with 397 additions and 16 deletions

View file

@ -1,17 +1,20 @@
package cy.agorise.bitsybitshareswallet.fragments package cy.agorise.bitsybitshareswallet.fragments
import android.graphics.Point
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager import android.preference.PreferenceManager
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import cy.agorise.bitsybitshareswallet.R import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.adapters.TransfersDetailsAdapter import cy.agorise.bitsybitshareswallet.adapters.TransfersDetailsAdapter
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
import cy.agorise.bitsybitshareswallet.utils.BounceTouchListener
import cy.agorise.bitsybitshareswallet.utils.Constants import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.viewmodels.TransferDetailViewModel import cy.agorise.bitsybitshareswallet.viewmodels.TransferDetailViewModel
import kotlinx.android.synthetic.main.fragment_transactions.* import kotlinx.android.synthetic.main.fragment_transactions.*
@ -20,6 +23,11 @@ class TransactionsFragment : Fragment() {
private lateinit var mTransferDetailViewModel: TransferDetailViewModel private lateinit var mTransferDetailViewModel: TransferDetailViewModel
/** Variables used for the RecyclerView pull springy animation */
private var bounceTouchListener: BounceTouchListener? = null
private var pivotY1: Float = 0.toFloat()
private var pivotY2:Float = 0.toFloat()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
@ -45,6 +53,45 @@ class TransactionsFragment : Fragment() {
mTransferDetailViewModel.getAll(userId).observe(this, Observer<List<TransferDetail>> { transfersDetails -> mTransferDetailViewModel.getAll(userId).observe(this, Observer<List<TransferDetail>> { transfersDetails ->
transfersDetailsAdapter.replaceAll(transfersDetails) transfersDetailsAdapter.replaceAll(transfersDetails)
}) })
rvTransactions.pivotX = getScreenWidth(activity) * 0.5f
pivotY1 = 0f
pivotY2 = getScreenHeight(activity) * .5f
bounceTouchListener =
BounceTouchListener.create(rvTransactions, object : BounceTouchListener.OnTranslateListener {
override fun onTranslate(translation: Float) {
if (translation > 0) {
bounceTouchListener?.setMaxAbsTranslation(-99)
rvTransactions.pivotY = pivotY1
val scale = 2 * translation / rvTransactions.measuredHeight + 1
rvTransactions.scaleY = Math.pow(scale.toDouble(), .6).toFloat()
} else {
bounceTouchListener?.setMaxAbsTranslation((pivotY2 * .33f).toInt())
rvTransactions.pivotY = pivotY2
val scale = 2 * translation / rvTransactions.measuredHeight + 1
rvTransactions.scaleY = Math.pow(scale.toDouble(), .5).toFloat()
}
}
})
// Sets custom touch listener to handle bounce/stretch effect
rvTransactions.setOnTouchListener(bounceTouchListener)
}
private fun getScreenWidth(activity: FragmentActivity?): Int {
val display = activity?.windowManager?.defaultDisplay
val size = Point()
display?.getSize(size)
return size.x
}
private fun getScreenHeight(activity: FragmentActivity?): Int {
val display = activity?.windowManager?.defaultDisplay
val size = Point()
display?.getSize(size)
return size.y
} }
// override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { // override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {

View file

@ -0,0 +1,340 @@
package cy.agorise.bitsybitshareswallet.utils
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator
import android.widget.ListView
import android.widget.ScrollView
import androidx.annotation.IdRes
import androidx.core.view.MotionEventCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
class BounceTouchListener private constructor(
private val mMainView: View,
contentResId: Int,
private val onTranslateListener: OnTranslateListener?
) :
View.OnTouchListener {
private var downCalled = false
private val mContent: View
private var mDownY: Float = 0.toFloat()
private var mSwipingDown: Boolean = false
private var mSwipingUp: Boolean = false
private val mInterpolator = DecelerateInterpolator(3f)
private val swipUpEnabled = true
private var mActivePointerId = -99
private var mLastTouchY = -99f
private var mMaxAbsTranslation = -99
init {
mContent = if (contentResId == -1) mMainView else mMainView.findViewById(contentResId)
}
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
val action = MotionEventCompat.getActionMasked(motionEvent)
when (action) {
MotionEvent.ACTION_DOWN -> {
run {
onDownMotionEvent(motionEvent)
view.onTouchEvent(motionEvent)
downCalled = true
if (mContent.translationY == 0f) {
return false
}
}
run {
if (mActivePointerId == -99) {
onDownMotionEvent(motionEvent)
downCalled = true
}
val pointerIndex = MotionEventCompat.findPointerIndex(motionEvent, mActivePointerId)
val y = MotionEventCompat.getY(motionEvent, pointerIndex)
if (!hasHitTop() && !hasHitBottom() || !downCalled) {
if (!downCalled) {
downCalled = true
}
mDownY = y
view.onTouchEvent(motionEvent)
return false
}
val deltaY = y - mDownY
if (Math.abs(deltaY) > 0 && hasHitTop() && deltaY > 0) {
mSwipingDown = true
sendCancelEventToView(view, motionEvent)
}
if (swipUpEnabled) {
if (Math.abs(deltaY) > 0 && hasHitBottom() && deltaY < 0) {
mSwipingUp = true
sendCancelEventToView(view, motionEvent)
}
}
if (mSwipingDown || mSwipingUp) {
if (deltaY <= 0 && mSwipingDown || deltaY >= 0 && mSwipingUp) {
mDownY = 0f
mSwipingDown = false
mSwipingUp = false
downCalled = false
val downEvent = MotionEvent.obtain(motionEvent)
downEvent.action = MotionEvent.ACTION_DOWN or
(MotionEventCompat.getActionIndex(motionEvent) shl MotionEventCompat.ACTION_POINTER_INDEX_SHIFT)
view.onTouchEvent(downEvent)
}
var translation =
(deltaY / Math.abs(deltaY) * Math.pow(Math.abs(deltaY).toDouble(), .8)).toInt()
if (mMaxAbsTranslation > 0) {
if (translation < 0) {
translation = Math.max(-mMaxAbsTranslation, translation)
} else {
translation = Math.min(mMaxAbsTranslation, translation)
}
}
mContent.translationY = translation.toFloat()
onTranslateListener?.onTranslate(mContent.translationY)
return true
}
}
}
MotionEvent.ACTION_MOVE -> {
if (mActivePointerId == -99) {
onDownMotionEvent(motionEvent)
downCalled = true
}
val pointerIndex = MotionEventCompat.findPointerIndex(motionEvent, mActivePointerId)
val y = MotionEventCompat.getY(motionEvent, pointerIndex)
if (!hasHitTop() && !hasHitBottom() || !downCalled) {
if (!downCalled) {
downCalled = true
}
mDownY = y
view.onTouchEvent(motionEvent)
return false
}
val deltaY = y - mDownY
if (Math.abs(deltaY) > 0 && hasHitTop() && deltaY > 0) {
mSwipingDown = true
sendCancelEventToView(view, motionEvent)
}
if (swipUpEnabled) {
if (Math.abs(deltaY) > 0 && hasHitBottom() && deltaY < 0) {
mSwipingUp = true
sendCancelEventToView(view, motionEvent)
}
}
if (mSwipingDown || mSwipingUp) {
if (deltaY <= 0 && mSwipingDown || deltaY >= 0 && mSwipingUp) {
mDownY = 0f
mSwipingDown = false
mSwipingUp = false
downCalled = false
val downEvent = MotionEvent.obtain(motionEvent)
downEvent.action = MotionEvent.ACTION_DOWN or
(MotionEventCompat.getActionIndex(motionEvent) shl MotionEventCompat.ACTION_POINTER_INDEX_SHIFT)
view.onTouchEvent(downEvent)
}
var translation = (deltaY / Math.abs(deltaY) * Math.pow(Math.abs(deltaY).toDouble(), .8)).toInt()
if (mMaxAbsTranslation > 0) {
if (translation < 0) {
translation = Math.max(-mMaxAbsTranslation, translation)
} else {
translation = Math.min(mMaxAbsTranslation, translation)
}
}
mContent.translationY = translation.toFloat()
onTranslateListener?.onTranslate(mContent.translationY)
return true
}
}
MotionEvent.ACTION_UP -> {
mActivePointerId = -99
// cancel
mContent.animate()
.setInterpolator(mInterpolator)
.translationY(0f)
.setDuration(DEFAULT_ANIMATION_TIME)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) {
(animation as ValueAnimator).addUpdateListener {
onTranslateListener?.onTranslate(mContent.translationY)
}
super.onAnimationStart(animation)
}
})
mDownY = 0f
mSwipingDown = false
mSwipingUp = false
downCalled = false
}
MotionEvent.ACTION_CANCEL -> {
mActivePointerId = -99
}
MotionEvent.ACTION_POINTER_UP -> {
val pointerIndex = MotionEventCompat.getActionIndex(motionEvent)
val pointerId = MotionEventCompat.getPointerId(motionEvent, pointerIndex)
if (pointerId == mActivePointerId) {
val newPointerIndex = if (pointerIndex == 0) 1 else 0
mLastTouchY = MotionEventCompat.getY(motionEvent, newPointerIndex)
mActivePointerId = MotionEventCompat.getPointerId(motionEvent, newPointerIndex)
if (mContent.translationY > 0) {
mDownY = mLastTouchY - Math.pow(mContent.translationY.toDouble(), (10f / 8f).toDouble()).toInt()
mContent.animate().cancel()
} else if (mContent.translationY < 0) {
mDownY = mLastTouchY +
Math.pow((-mContent.translationY).toDouble(), (10f / 8f).toDouble()).toInt()
mContent.animate().cancel()
}
}
}
}
return false
}
private fun sendCancelEventToView(view: View, motionEvent: MotionEvent) {
(view as ViewGroup).requestDisallowInterceptTouchEvent(true)
val cancelEvent = MotionEvent.obtain(motionEvent)
cancelEvent.action = MotionEvent.ACTION_CANCEL or
(MotionEventCompat.getActionIndex(motionEvent) shl MotionEventCompat.ACTION_POINTER_INDEX_SHIFT)
view.onTouchEvent(cancelEvent)
}
private fun onDownMotionEvent(motionEvent: MotionEvent) {
val pointerIndex = MotionEventCompat.getActionIndex(motionEvent)
mLastTouchY = MotionEventCompat.getY(motionEvent, pointerIndex)
mActivePointerId = MotionEventCompat.getPointerId(motionEvent, 0)
if (mContent.translationY > 0) {
mDownY = mLastTouchY - Math.pow(mContent.translationY.toDouble(), (10f / 8f).toDouble()).toInt()
mContent.animate().cancel()
} else if (mContent.translationY < 0) {
mDownY = mLastTouchY + Math.pow((-mContent.translationY).toDouble(), (10f / 8f).toDouble()).toInt()
mContent.animate().cancel()
} else {
mDownY = mLastTouchY
}
}
private fun hasHitBottom(): Boolean {
if (mMainView is ScrollView) {
val scrollView = mMainView
val view = scrollView.getChildAt(scrollView.childCount - 1)
val diff = view.bottom - (scrollView.height + scrollView.scrollY)// Calculate the scrolldiff
return diff == 0
} else if (mMainView is ListView) {
val listView = mMainView
if (listView.adapter != null) {
if (listView.adapter.count > 0) {
return listView.lastVisiblePosition == listView.adapter.count - 1 && listView.getChildAt(listView.childCount - 1).bottom <= listView.height
}
}
} else if (mMainView is RecyclerView) {
val recyclerView = mMainView
if (recyclerView.adapter != null && recyclerView.layoutManager != null) {
val adapter = recyclerView.adapter
if (adapter!!.itemCount > 0) {
val layoutManager = recyclerView.layoutManager
if (layoutManager is LinearLayoutManager) {
val linearLayoutManager = layoutManager as LinearLayoutManager?
return linearLayoutManager!!.findLastCompletelyVisibleItemPosition() == adapter.itemCount - 1
} else if (layoutManager is StaggeredGridLayoutManager) {
val staggeredGridLayoutManager = layoutManager as StaggeredGridLayoutManager?
val checks = staggeredGridLayoutManager!!.findLastCompletelyVisibleItemPositions(null)
for (check in checks) {
if (check == adapter.itemCount - 1)
return true
}
}
}
}
}
return false
}
private fun hasHitTop(): Boolean {
if (mMainView is ScrollView) {
return mMainView.scrollY == 0
} else if (mMainView is ListView) {
val listView = mMainView
if (listView.adapter != null) {
if (listView.adapter.count > 0) {
return listView.firstVisiblePosition == 0 && listView.getChildAt(0).top >= 0
}
}
} else if (mMainView is RecyclerView) {
val recyclerView = mMainView
if (recyclerView.adapter != null && recyclerView.layoutManager != null) {
val adapter = recyclerView.adapter
if (adapter!!.itemCount > 0) {
val layoutManager = recyclerView.layoutManager
if (layoutManager is LinearLayoutManager) {
val linearLayoutManager = layoutManager as LinearLayoutManager?
return linearLayoutManager!!.findFirstCompletelyVisibleItemPosition() == 0
} else if (layoutManager is StaggeredGridLayoutManager) {
val staggeredGridLayoutManager = layoutManager as StaggeredGridLayoutManager?
val checks = staggeredGridLayoutManager!!.findFirstCompletelyVisibleItemPositions(null)
for (check in checks) {
if (check == 0)
return true
}
}
}
}
}
return false
}
fun setMaxAbsTranslation(maxAbsTranslation: Int) {
this.mMaxAbsTranslation = maxAbsTranslation
}
interface OnTranslateListener {
fun onTranslate(translation: Float)
}
companion object {
private val DEFAULT_ANIMATION_TIME = 600L
/**
* 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
*/
fun create(mainScrollableView: View, onTranslateListener: OnTranslateListener?): BounceTouchListener {
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
*/
fun create(
mainView: View, @IdRes contentResId: Int,
onTranslateListener: OnTranslateListener?
): BounceTouchListener {
return BounceTouchListener(mainView, contentResId, onTranslateListener)
}
}
}

View file

@ -1,18 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rvTransactions"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:padding="@dimen/card_margin"
<androidx.recyclerview.widget.RecyclerView android:clipToPadding="false"
android:id="@+id/rvTransactions" tools:listitem="@layout/item_transaction"
android:layout_width="match_parent" tools:itemCount="6"
android:layout_height="wrap_content" android:layoutAnimation="@anim/layout_animation_from_bottom"/>
android:padding="@dimen/card_margin"
android:clipToPadding="false"
tools:listitem="@layout/item_transaction"
tools:itemCount="6"
android:layoutAnimation="@anim/layout_animation_from_bottom"/>
</LinearLayout>