Simplify BounceTouchListener and TransactionsFragment, to produce the same bouncy effect when overscrolling the transactions list, but with much less code.

This commit is contained in:
Severiano Jaramillo 2018-12-23 21:51:46 -06:00
parent 6ef5d84394
commit 4391ff5ad4
2 changed files with 63 additions and 229 deletions

View file

@ -1,11 +1,9 @@
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.* import android.view.*
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
@ -21,11 +19,6 @@ 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?
@ -52,47 +45,11 @@ class TransactionsFragment : Fragment() {
transfersDetailsAdapter.replaceAll(transfersDetails) transfersDetailsAdapter.replaceAll(transfersDetails)
}) })
// Create bouncy effect when user tries to over scroll
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 // Sets custom touch listener to handle bounce/stretch effect
val bounceTouchListener = BounceTouchListener(rvTransactions)
rvTransactions.setOnTouchListener(bounceTouchListener) 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) {
inflater.inflate(R.menu.menu_transactions, menu) inflater.inflate(R.menu.menu_transactions, menu)
} }

View file

@ -7,99 +7,36 @@ import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator import android.view.animation.DecelerateInterpolator
import android.widget.ListView
import android.widget.ScrollView
import androidx.annotation.IdRes
import androidx.core.view.MotionEventCompat import androidx.core.view.MotionEventCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager import androidx.recyclerview.widget.StaggeredGridLayoutManager
class BounceTouchListener private constructor( class BounceTouchListener(private val mRecyclerView: RecyclerView) : View.OnTouchListener {
private val mMainView: View,
contentResId: Int,
private val onTranslateListener: OnTranslateListener?
) :
View.OnTouchListener {
private var downCalled = false private var downCalled = false
private val mContent: View = if (contentResId == -1) mMainView else mMainView.findViewById(contentResId)
private var mDownY: Float = 0.toFloat() private var mDownY: Float = 0.toFloat()
private var mSwipingDown: Boolean = false private var mSwipingDown: Boolean = false
private var mSwipingUp: Boolean = false private var mSwipingUp: Boolean = false
private val mInterpolator = DecelerateInterpolator(3f) private val mInterpolator = DecelerateInterpolator(3f)
private val swipUpEnabled = true
private var mActivePointerId = -99 private var mActivePointerId = -99
private var mLastTouchY = -99f private var mLastTouchY = -99f
private var mMaxAbsTranslation = -99 private var mMaxAbsTranslation = -99
init {
mRecyclerView.pivotY = 0F
}
override fun onTouch(view: View, motionEvent: MotionEvent): Boolean { override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {
val action = MotionEventCompat.getActionMasked(motionEvent) val action = MotionEventCompat.getActionMasked(motionEvent)
when (action) { when (action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
run { onDownMotionEvent(motionEvent)
onDownMotionEvent(motionEvent) view.onTouchEvent(motionEvent)
view.onTouchEvent(motionEvent) downCalled = true
downCalled = true if (this.mRecyclerView.translationY == 0f) {
if (mContent.translationY == 0f) { return false
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 -> { MotionEvent.ACTION_MOVE -> {
@ -122,11 +59,9 @@ class BounceTouchListener private constructor(
mSwipingDown = true mSwipingDown = true
sendCancelEventToView(view, motionEvent) sendCancelEventToView(view, motionEvent)
} }
if (swipUpEnabled) { if (Math.abs(deltaY) > 0 && hasHitBottom() && deltaY < 0) {
if (Math.abs(deltaY) > 0 && hasHitBottom() && deltaY < 0) { mSwipingUp = true
mSwipingUp = true sendCancelEventToView(view, motionEvent)
sendCancelEventToView(view, motionEvent)
}
} }
if (mSwipingDown || mSwipingUp) { if (mSwipingDown || mSwipingUp) {
if (deltaY <= 0 && mSwipingDown || deltaY >= 0 && mSwipingUp) { if (deltaY <= 0 && mSwipingDown || deltaY >= 0 && mSwipingUp) {
@ -147,8 +82,8 @@ class BounceTouchListener private constructor(
translation = Math.min(mMaxAbsTranslation, translation) translation = Math.min(mMaxAbsTranslation, translation)
} }
} }
mContent.translationY = translation.toFloat() mRecyclerView.translationY = translation.toFloat()
onTranslateListener?.onTranslate(mContent.translationY) translate(mRecyclerView.translationY)
return true return true
} }
} }
@ -156,14 +91,14 @@ class BounceTouchListener private constructor(
MotionEvent.ACTION_UP -> { MotionEvent.ACTION_UP -> {
mActivePointerId = -99 mActivePointerId = -99
// cancel // cancel
mContent.animate() mRecyclerView.animate()
.setInterpolator(mInterpolator) .setInterpolator(mInterpolator)
.translationY(0f) .translationY(0f)
.setDuration(DEFAULT_ANIMATION_TIME) .setDuration(600L)
.setListener(object : AnimatorListenerAdapter() { .setListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) { override fun onAnimationStart(animation: Animator) {
(animation as ValueAnimator).addUpdateListener { (animation as ValueAnimator).addUpdateListener {
onTranslateListener?.onTranslate(mContent.translationY) translate(mRecyclerView.translationY)
} }
super.onAnimationStart(animation) super.onAnimationStart(animation)
} }
@ -188,13 +123,13 @@ class BounceTouchListener private constructor(
mLastTouchY = MotionEventCompat.getY(motionEvent, newPointerIndex) mLastTouchY = MotionEventCompat.getY(motionEvent, newPointerIndex)
mActivePointerId = MotionEventCompat.getPointerId(motionEvent, newPointerIndex) mActivePointerId = MotionEventCompat.getPointerId(motionEvent, newPointerIndex)
if (mContent.translationY > 0) { if (this.mRecyclerView.translationY > 0) {
mDownY = mLastTouchY - Math.pow(mContent.translationY.toDouble(), (10f / 8f).toDouble()).toInt() mDownY = mLastTouchY - Math.pow(this.mRecyclerView.translationY.toDouble(), (10f / 8f).toDouble()).toInt()
mContent.animate().cancel() this.mRecyclerView.animate().cancel()
} else if (mContent.translationY < 0) { } else if (this.mRecyclerView.translationY < 0) {
mDownY = mLastTouchY + mDownY = mLastTouchY +
Math.pow((-mContent.translationY).toDouble(), (10f / 8f).toDouble()).toInt() Math.pow((-this.mRecyclerView.translationY).toDouble(), (10f / 8f).toDouble()).toInt()
mContent.animate().cancel() this.mRecyclerView.animate().cancel()
} }
} }
} }
@ -202,6 +137,11 @@ class BounceTouchListener private constructor(
return false return false
} }
private fun translate(translation: Float) {
val scale = 2 * translation / mRecyclerView.measuredHeight + 1
mRecyclerView.scaleY = Math.pow(scale.toDouble(), .6).toFloat()
}
private fun sendCancelEventToView(view: View, motionEvent: MotionEvent) { private fun sendCancelEventToView(view: View, motionEvent: MotionEvent) {
(view as ViewGroup).requestDisallowInterceptTouchEvent(true) (view as ViewGroup).requestDisallowInterceptTouchEvent(true)
val cancelEvent = MotionEvent.obtain(motionEvent) val cancelEvent = MotionEvent.obtain(motionEvent)
@ -215,46 +155,32 @@ class BounceTouchListener private constructor(
mLastTouchY = MotionEventCompat.getY(motionEvent, pointerIndex) mLastTouchY = MotionEventCompat.getY(motionEvent, pointerIndex)
mActivePointerId = MotionEventCompat.getPointerId(motionEvent, 0) mActivePointerId = MotionEventCompat.getPointerId(motionEvent, 0)
if (mContent.translationY > 0) { if (this.mRecyclerView.translationY > 0) {
mDownY = mLastTouchY - Math.pow(mContent.translationY.toDouble(), (10f / 8f).toDouble()).toInt() mDownY = mLastTouchY - Math.pow(this.mRecyclerView.translationY.toDouble(), (10f / 8f).toDouble()).toInt()
mContent.animate().cancel() this.mRecyclerView.animate().cancel()
} else if (mContent.translationY < 0) { } else if (this.mRecyclerView.translationY < 0) {
mDownY = mLastTouchY + Math.pow((-mContent.translationY).toDouble(), (10f / 8f).toDouble()).toInt() mDownY = mLastTouchY + Math.pow((-this.mRecyclerView.translationY).toDouble(), (10f / 8f).toDouble()).toInt()
mContent.animate().cancel() this.mRecyclerView.animate().cancel()
} else { } else {
mDownY = mLastTouchY mDownY = mLastTouchY
} }
} }
private fun hasHitBottom(): Boolean { private fun hasHitBottom(): Boolean {
if (mMainView is ScrollView) { val recyclerView = this.mRecyclerView
val scrollView = mMainView if (recyclerView.adapter != null && recyclerView.layoutManager != null) {
val view = scrollView.getChildAt(scrollView.childCount - 1) val adapter = recyclerView.adapter
val diff = view.bottom - (scrollView.height + scrollView.scrollY)// Calculate the scrolldiff if (adapter!!.itemCount > 0) {
return diff == 0 val layoutManager = recyclerView.layoutManager
} else if (mMainView is ListView) { if (layoutManager is LinearLayoutManager) {
val listView = mMainView val linearLayoutManager = layoutManager as LinearLayoutManager?
if (listView.adapter != null) { return linearLayoutManager!!.findLastCompletelyVisibleItemPosition() == adapter.itemCount - 1
if (listView.adapter.count > 0) { } else if (layoutManager is StaggeredGridLayoutManager) {
return listView.lastVisiblePosition == listView.adapter.count - 1 && listView.getChildAt(listView.childCount - 1).bottom <= listView.height val staggeredGridLayoutManager = layoutManager as StaggeredGridLayoutManager?
} val checks = staggeredGridLayoutManager!!.findLastCompletelyVisibleItemPositions(null)
} for (check in checks) {
} else if (mMainView is RecyclerView) { if (check == adapter.itemCount - 1)
val recyclerView = mMainView return true
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
}
} }
} }
} }
@ -263,31 +189,20 @@ class BounceTouchListener private constructor(
} }
private fun hasHitTop(): Boolean { private fun hasHitTop(): Boolean {
if (mMainView is ScrollView) { val recyclerView = this.mRecyclerView
return mMainView.scrollY == 0 if (recyclerView.adapter != null && recyclerView.layoutManager != null) {
} else if (mMainView is ListView) { val adapter = recyclerView.adapter
val listView = mMainView if (adapter!!.itemCount > 0) {
if (listView.adapter != null) { val layoutManager = recyclerView.layoutManager
if (listView.adapter.count > 0) { if (layoutManager is LinearLayoutManager) {
return listView.firstVisiblePosition == 0 && listView.getChildAt(0).top >= 0 val linearLayoutManager = layoutManager as LinearLayoutManager?
} return linearLayoutManager!!.findFirstCompletelyVisibleItemPosition() == 0
} } else if (layoutManager is StaggeredGridLayoutManager) {
} else if (mMainView is RecyclerView) { val staggeredGridLayoutManager = layoutManager as StaggeredGridLayoutManager?
val recyclerView = mMainView val checks = staggeredGridLayoutManager!!.findFirstCompletelyVisibleItemPositions(null)
if (recyclerView.adapter != null && recyclerView.layoutManager != null) { for (check in checks) {
val adapter = recyclerView.adapter if (check == 0)
if (adapter!!.itemCount > 0) { return true
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
}
} }
} }
} }
@ -295,42 +210,4 @@ class BounceTouchListener private constructor(
return false 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)
}
}
} }