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
import android.graphics.Point
import android.os.Bundle
import android.preference.PreferenceManager
import android.view.*
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
@ -21,11 +19,6 @@ class TransactionsFragment : Fragment() {
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(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@ -52,47 +45,11 @@ class TransactionsFragment : Fragment() {
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
val bounceTouchListener = BounceTouchListener(rvTransactions)
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) {
inflater.inflate(R.menu.menu_transactions, menu)
}

View file

@ -7,99 +7,36 @@ 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 {
class BounceTouchListener(private val mRecyclerView: RecyclerView) : View.OnTouchListener {
private var downCalled = false
private val mContent: View = if (contentResId == -1) mMainView else mMainView.findViewById(contentResId)
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 {
mRecyclerView.pivotY = 0F
}
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
}
onDownMotionEvent(motionEvent)
view.onTouchEvent(motionEvent)
downCalled = true
if (this.mRecyclerView.translationY == 0f) {
return false
}
}
MotionEvent.ACTION_MOVE -> {
@ -122,11 +59,9 @@ class BounceTouchListener private constructor(
mSwipingDown = true
sendCancelEventToView(view, motionEvent)
}
if (swipUpEnabled) {
if (Math.abs(deltaY) > 0 && hasHitBottom() && deltaY < 0) {
mSwipingUp = true
sendCancelEventToView(view, motionEvent)
}
if (Math.abs(deltaY) > 0 && hasHitBottom() && deltaY < 0) {
mSwipingUp = true
sendCancelEventToView(view, motionEvent)
}
if (mSwipingDown || mSwipingUp) {
if (deltaY <= 0 && mSwipingDown || deltaY >= 0 && mSwipingUp) {
@ -147,8 +82,8 @@ class BounceTouchListener private constructor(
translation = Math.min(mMaxAbsTranslation, translation)
}
}
mContent.translationY = translation.toFloat()
onTranslateListener?.onTranslate(mContent.translationY)
mRecyclerView.translationY = translation.toFloat()
translate(mRecyclerView.translationY)
return true
}
}
@ -156,14 +91,14 @@ class BounceTouchListener private constructor(
MotionEvent.ACTION_UP -> {
mActivePointerId = -99
// cancel
mContent.animate()
mRecyclerView.animate()
.setInterpolator(mInterpolator)
.translationY(0f)
.setDuration(DEFAULT_ANIMATION_TIME)
.setDuration(600L)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator) {
(animation as ValueAnimator).addUpdateListener {
onTranslateListener?.onTranslate(mContent.translationY)
translate(mRecyclerView.translationY)
}
super.onAnimationStart(animation)
}
@ -188,13 +123,13 @@ class BounceTouchListener private constructor(
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) {
if (this.mRecyclerView.translationY > 0) {
mDownY = mLastTouchY - Math.pow(this.mRecyclerView.translationY.toDouble(), (10f / 8f).toDouble()).toInt()
this.mRecyclerView.animate().cancel()
} else if (this.mRecyclerView.translationY < 0) {
mDownY = mLastTouchY +
Math.pow((-mContent.translationY).toDouble(), (10f / 8f).toDouble()).toInt()
mContent.animate().cancel()
Math.pow((-this.mRecyclerView.translationY).toDouble(), (10f / 8f).toDouble()).toInt()
this.mRecyclerView.animate().cancel()
}
}
}
@ -202,6 +137,11 @@ class BounceTouchListener private constructor(
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) {
(view as ViewGroup).requestDisallowInterceptTouchEvent(true)
val cancelEvent = MotionEvent.obtain(motionEvent)
@ -215,46 +155,32 @@ class BounceTouchListener private constructor(
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()
if (this.mRecyclerView.translationY > 0) {
mDownY = mLastTouchY - Math.pow(this.mRecyclerView.translationY.toDouble(), (10f / 8f).toDouble()).toInt()
this.mRecyclerView.animate().cancel()
} else if (this.mRecyclerView.translationY < 0) {
mDownY = mLastTouchY + Math.pow((-this.mRecyclerView.translationY).toDouble(), (10f / 8f).toDouble()).toInt()
this.mRecyclerView.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
}
val recyclerView = this.mRecyclerView
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 {
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
}
val recyclerView = this.mRecyclerView
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
}
}
}
@ -295,42 +210,4 @@ class BounceTouchListener private constructor(
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)
}
}
}