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:
parent
6ef5d84394
commit
4391ff5ad4
2 changed files with 63 additions and 229 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,101 +7,38 @@ 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 (mContent.translationY == 0f) {
|
if (this.mRecyclerView.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 -> {
|
||||||
if (mActivePointerId == -99) {
|
if (mActivePointerId == -99) {
|
||||||
onDownMotionEvent(motionEvent)
|
onDownMotionEvent(motionEvent)
|
||||||
|
@ -122,12 +59,10 @@ 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) {
|
||||||
mDownY = 0f
|
mDownY = 0f
|
||||||
|
@ -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,32 +155,19 @@ 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
|
|
||||||
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) {
|
if (recyclerView.adapter != null && recyclerView.layoutManager != null) {
|
||||||
val adapter = recyclerView.adapter
|
val adapter = recyclerView.adapter
|
||||||
if (adapter!!.itemCount > 0) {
|
if (adapter!!.itemCount > 0) {
|
||||||
|
@ -258,22 +185,11 @@ class BounceTouchListener private constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasHitTop(): Boolean {
|
private fun hasHitTop(): Boolean {
|
||||||
if (mMainView is ScrollView) {
|
val recyclerView = this.mRecyclerView
|
||||||
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) {
|
if (recyclerView.adapter != null && recyclerView.layoutManager != null) {
|
||||||
val adapter = recyclerView.adapter
|
val adapter = recyclerView.adapter
|
||||||
if (adapter!!.itemCount > 0) {
|
if (adapter!!.itemCount > 0) {
|
||||||
|
@ -291,46 +207,7 @@ 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
Loading…
Reference in a new issue