Intercepting touch event and re-directing it depending on motion event state - android

I've created a custom view in Android that inherits from RelativeLayout. Inside this view is an OverScroller which is used to handle scrolling of the view when a touch event occurs:
class MyCustomView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : RelativeLayout(context, attrs, defStyleAttr) {
constructor(context: Context) : this(context, null, 0)
constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0)
private val scroller = OverScroller(context, FastOutLinearInInterpolator())
private var currentY = 0
private val gestureDetector = GestureDetectorCompat(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onDown(event: MotionEvent?): Boolean {
// Stop the current scroll animation
scroller.forceFinished(true)
// Invalidate the view
ViewCompat.postInvalidateOnAnimation(this#MyCustomView)
return true
}
override fun onScroll(event1: MotionEvent?, event2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
scroller.forceFinished(true)
currentY -= distanceY.toInt()
ViewCompat.postInvalidateOnAnimation(this#MyCustomView)
return true
}
override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
// Stop any scrolling
scroller.forceFinished(true)
scroller.fling(0, currentY, 0, velocityY.toInt(), 0, 0,
-5000 + height, 0)
// Invalidate the view
ViewCompat.postInvalidateOnAnimation(this#MyCustomView)
return true
}
})
#SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean {
if (event == null) return false
// Return the value from the gesture detector
return gestureDetector.onTouchEvent(event)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
// Draw stuff to canvas, using currentY as origin
}
override fun computeScroll() {
// Call to super class method
super.computeScroll()
if (scroller.computeScrollOffset()) {
currentY = scroller.currY
}
// Invalidate the view
ViewCompat.postInvalidateOnAnimation(this#MyCustomView)
}
}
This works fine. Next, I added a RelativeLayout view group to hold a load of inflated cells. These cells are clickable. Now, when I try to initiate a scroll with the first touch on one of these inflated cells, the cell swallows the click event (the parent view with the scroller never get's it's onTouchEvent called and therefore doesn't scroll).
Important to note: The RelativeLayout view group which holds the cells is a child view of MyCustomView.
I've tried overriding onInterceptTouchEvent in MyCustomView in order to intercept the touch event before it is dispatched to either view like so:
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
if (event == null) return false
if (event.action == MotionEvent.ACTION_UP) {
if (event.historySize > 0) {
val startY = event.getHistoricalY(0)
val endY = event.getHistoricalY(event.historySize - 1)
val distance = Math.abs(endY - startY).toInt()
if (distance == 0) {
// Consider this a click (so don't swallow it
return false
}
}
}
// Ok this isn't a click, so call this views onTouchEvent
return this.onTouchEvent(event)
}
I then determine if the touch event's action code is ACTION_UP, if it is I calculate the total distance between ACTION_DOWN and ACTION_UP and, if it's in a small enough range, I treat it as a click and call super.onInterceptTouchEvent(event) (which will click an inflated cell if the event was on a cell, else it will end up calling my onTouchEvent which will handle scrolling. If the event's action code was not ACTION_UP OR the distance moved was not small enough, it will return this.onTouchEvent(event).
The problem I'm having is that my solution isn't working, scrolling still cannot be initiated when starting the scroll on top of an inflated cell.
Does anyone know what I'm missing?

Related

How to dismiss Bottom Sheet fragment when click outside in Kotlin?

I make bottom sheet fragment like this:
val bottomSheet = PictureBottomSheetFragment(fragment)
bottomSheet.isCancelable = true
bottomSheet.setListener(pictureListener)
bottomSheet.show(ac.supportFragmentManager, "PictureBottomSheetFragment")
But its not dismiss when I touch outside. and dismiss or isCancelable not working.
try this
behavior.setState(BottomSheetBehavior.STATE_HIDDEN));
You can override method and indicate, for example, in onViewCreated what you need:
class ModalDialogSuccsesDataPatient : ModalDialog() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
isCancelable = false //or true
}
}
Let's try to design reusable functions to solve this problem and similar ones if the need arises.
We can create extension functions on View that tell whether a point on the screen is contained within the View or not.
fun View.containsPoint(rawX: Int, rawY: Int): Boolean {
val rect = Rect()
this.getGlobalVisibleRect(rect)
return rect.contains(rawX, rawY)
}
fun View.doesNotContainPoint(rawX: Int, rawY: Int) = !containsPoint(rawX, rawY)
Now we can override the dispatchTouchEvent(event: MotionEvent) method of Activity to know where exactly the user clicked on the screen.
private const val SCROLL_THRESHOLD = 10F // To filter out scroll gestures from clicks
private var downX = 0F
private var downY = 0F
private var isClick = false
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
downX = event.x
downY = event.y
isClick = true
}
MotionEvent.ACTION_MOVE -> {
val xThreshCrossed = abs(downX - event.x) > SCROLL_THRESHOLD
val yThreshCrossed = abs(downY - event.y) > SCROLL_THRESHOLD
if (isClick and (xThreshCrossed or yThreshCrossed)) {
isClick = false
}
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
if (isClick) onScreenClick(event.rawX, event.rawY)
}
else -> { }
}
return super.dispatchTouchEvent(event)
}
private fun onScreenClick(rawX: Float, rawY: Float) { }
Now, you can simply use the above-defined functions to achieve the required result
private fun onScreenClick(rawX: Float, rawY: Float) {
if (bottomSheet.doesNotContainPoint(rawX.toInt(), rawY.toInt())) {
// Handle bottomSheet state changes
}
}
What more? If you have a BaseActivity which is extended by all your Activities then you can add the click detection code to it. You can make the onScreenClick an protected open method so that it can be overridden by the sub-classes.
protected open fun onScreenClick(rawX: Float, rawY: Float) { }
Usage:
override fun onScreenClick(rawX: Float, rawY: Float) {
super.onScreenClick(rawX, rawY)
if (bottomSheet.doesNotContainPoint(rawX.toInt(), rawY.toInt())) {
// Handle bottomSheet state changes
}
}

Android ViewPager inside SwipeRefreshLayout

I have a ViewPager with three fragments inside of a SwipeRefreshLayout. The problem I'm seeing is that when attempting to swipe between fragments in the ViewPager, sometimes the SwipeRefreshLayout will take over and stop the interaction with the ViewPager. It appears to do so if the gesture for the ViewPager interaction goes vertical even a tiny bit.
I found a few questions on StackOverflow that were somewhat similar to mine, but none of them quite fit comprehensively. Here's my solution.
A custom SwipeRefreshLayout that can toggle its InterceptTouchEvent:
class ToggleableSwipeRefreshLayout : SwipeRefreshLayout {
private var isDisabled = false
private var touchSlop = ViewConfiguration.get(context).scaledTouchSlop
private var prevX = 0f
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
fun setDisabled(isDisabled: Boolean) {
this.isDisabled = isDisabled
parent.requestDisallowInterceptTouchEvent(isDisabled)
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
val event = MotionEvent.obtain(ev)
prevX = event.x
event.recycle()
}
MotionEvent.ACTION_MOVE -> {
if (isDisabled) { return false }
val eventX = ev.x
val xDiff = Math.abs(eventX - prevX)
if (xDiff > touchSlop) {
return false
}
}
}
return super.onInterceptTouchEvent(ev)
}
}
And in the Fragment/Activity this is being used in, add an OnPageChangeListener to the ViewPager to monitor the scroll state and enable/disable the SwipeRefreshLayout's InterceptTouchEvent accordingly:
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
when (state) {
ViewPager.SCROLL_STATE_DRAGGING -> swipeLayout.setDisabled(true)
ViewPager.SCROLL_STATE_IDLE -> swipeLayout.setDisabled(false)
}
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { }
override fun onPageSelected(position: Int) { }
})
Now it works great!

Android VIewGroup handle touch, child handle click

I have i custom ViewPager that detects taps, long press and long press up events with a GestureDetector. It also allows to wipe ViewPager.
Gesture listener is simple:
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent?) = true
override fun onLongPress(e: MotionEvent?) {
mTouchListener.invoke(TAPEVENT.LONGTAP)
mWasLongTap = true
}
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
mTouchListener.invoke(TAPEVENT.TAP)
return true
}
override fun onSingleTapUp(e: MotionEvent?): Boolean {
if (mWasLongTap) {
mWasLongTap = false
mTouchListener.invoke(TAPEVENT.LONGTAPUP)
}
return true
}
}
//override view group methods
override fun onTouchEvent(ev: MotionEvent?): Boolean {
super.onTouchEvent(ev)
return mDetector.onTouchEvent(ev)
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
mDetector.onTouchEvent(ev)
return super.dispatchTouchEvent(ev)
}
And i have fragments inside a ViewPager. In my fragments i have, lets say, a Button view.
When my button is clicked onSingleTapConfirmed event is fired too.
Cannot figure out what to do to force ViewPager do not process event if there was a click on a child view of a fragment.
Fragment view looks something like this:
<CoordinatorLayout>
<View>
</View>
<Button>
</Button>
</CoordinatorLayout>
I ended up with such a solution.
GestureListener is the same, but onTouchEvent is overridden the other way.
override fun onTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_UP && mWasLongTap) {
mWasLongTap = false
mTouchListener.invoke(TAPEVENT.LONGTAPUP)
}
return mDetector.onTouchEvent(ev) || super.onTouchEvent(ev)
}

Intercept motion event from RecyclerView in opened PopupWindow

In my case I want to open PopupWindow by long press on ViewHolder item and process motion event in this window without removing finger. How can I achieve this?
I trying to open CustomPopupWindow by follow:
override fun onBindViewHolder(holder: Item, position: Int) {
val item = items[position]
holder.bindView(testItem)
holder.itemView.view.setOnLongClickListener {
val inflater = LayoutInflater.from(parent?.context)
val view = inflater.inflate(R.layout.popup_window, null)
val popupMenu = CustomPopupWindow(view, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
popupMenu.elevation = 5f
popupMenu.showAsDropDown(holder.itemView.view)
true
}
}
and after that disable scrolling in RecyclerView:
class CustomLayoutManager(context: Context) : LinearLayoutManager(context) {
var scrollEnabled: Boolean = true
override fun canScrollVertically(): Boolean {
return scrollEnabled
}
}
Here my CustomPopupWindow:
class CustomPopupWindow(contentView: View?, width: Int, height: Int) : PopupWindow(contentView, width, height), View.OnTouchListener {
init {
contentView?.setOnTouchListener(this)
setTouchInterceptor(this)
}
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
Log.i("Touch", "Touch")
}
MotionEvent.ACTION_MOVE -> {
Log.i("Touch", "Event {${event.x}; ${event.y}}")
}
MotionEvent.ACTION_UP-> {
Log.i("Touch", "Up")
}
}
return true
}
}
In this case onTouch() event never called in CustomPopupWindow only if I remove finger and tap again.
Thanks advance!
SOLVED
I solved this by adding a touch listener to the anchor view:
holder.itemView.view.setOnLongClickListener {
val inflater = LayoutInflater.from(parent?.context)
val view = inflater.inflate(R.layout.popup_window, null)
val popupMenu = CustomPopupWindow(view, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
popupMenu.elevation = 5f
it.setOnTouchListener(popupMenu) // solution
popupMenu.showAsDropDown(it)
true
}
Thanks #Brucelet
If you can refactor to using a PopupMenu, then I think PopupMenu.getDragToOpenListener() will do what you want. Similar for ListPopupWindow.createDragToOpenListener().
You could also look at the implementation of those methods for inspiration in creating your own.

Android Kotlin child onClick blocks parent OnTouch

I have this layout hierarchy:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/parent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.xxxxxx.Widget
android:id="#+id/widget1"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<com.xxxxxx.Widget
android:id="#+id/widget2"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
I have touch Events for the Parent LinearLayout like this:
parent.setOnTouchListener(myCustomTouchParent)
class MyCustomTouchParent(ctx: Context): View.OnTouchListener {
private var isScrollingDown = false
private var isScrollingUp = false
private val myGestureDetected = GestureDetector(ctx, MyGestureListener())
var onRecyclerViewMovingDown: (() -> Unit)? = null
override fun onTouch(p0: View?, e: MotionEvent): Boolean {
myGestureDetected.onTouchEvent(e)
when(e.action){
MotionEvent.ACTION_UP -> {
if (isScrollingDown) {
onRecyclerViewMovingDown?.invoke()
}
}
MotionEvent.ACTION_DOWN -> {
Log.i("TAg", "Action Down")
isScrollingDown = false
isScrollingUp = false
}
}
return true
}
inner class MyGestureListener: GestureDetector.SimpleOnGestureListener() {
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
if(e2.y - e1.y > 0){
isScrollingUp = true
} else if(e2.y - e1.y < 0){
isScrollingDown = true
}
return super.onScroll(e1, e2, distanceX, distanceY)
}
}
}
Basically, this will detect a 'Scroll Up' event on the parent, and will perform some animations. The problem is, as soon as I set a click listener for widget1 and widget2, the touch event of the parent is no longer working. Is there any workaround for this?
The only thing that worked for me: In the parent LinearLayout, intercept the touch, call onTouchEvent and return false:
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
onTouchEvent(ev)
return false
}
Move the TouchInterceptor with the Gesture Detector class to the parent onTouchEvent:
override fun onTouchEvent(e: MotionEvent): Boolean {
myGestureDetected.onTouchEvent(e)
when(e.action){
MotionEvent.ACTION_UP -> {
if (isScrollingDown) {
onRecyclerViewMovingDown?.invoke()
}
}
MotionEvent.ACTION_DOWN -> {
isScrollingDown = false
isScrollingUp = false
}
}
return super.onTouchEvent(e)
}
I don't know if there is a better solution, but this one let me handle touch event on the parent first, then it passed the touch to the childs. There you can set your click listeners.
Also, if you don't set click listeners, the area of touch that contains the clickable item won't trigger touch. so, better set
clickable=true
in all the items, and then only set listeners when you need.
You have to override both onTouchListeners on your children views and return false, that will make them not override their parent ontouch.
widget1.onTouch { view, motionEvent -> return#onTouch false }
widget2.onTouch { view, motionEvent -> return#onTouch false }

Categories

Resources