How to intercept all touch events? - android

How can I get an "top" View of my application (which contains Activity and all DialogFragments)? I need to intercept all touch events to handle motion of some View between DialogFragment and my Activity.
I've tried to catch them (event) through the decor view of Activity's Window with no luck:
getWindow().getDecorView().setOnTouchListener(...);

You can override the Activity.dispatchTouchEvent to intercept all touch events in your activity, even if you have some Views like ScrollView, Button, etc that will consume the touch event.
Combining withViewGroup.requestDisallowInterceptTouchEvent, you can disable the touch event of the ViewGroup. For example, if you want to disable all the touch event in some ViewGroup, try this:
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
requestDisallowInterceptTouchEvent(
(ViewGroup) findViewById(R.id.topLevelRelativeLayout),
true
);
return super.dispatchTouchEvent(event);
}
private void requestDisallowInterceptTouchEvent(ViewGroup v, boolean disallowIntercept) {
v.requestDisallowInterceptTouchEvent(disallowIntercept);
int childCount = v.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = v.getChildAt(i);
if (child instanceof ViewGroup) {
requestDisallowInterceptTouchEvent((ViewGroup) child, disallowIntercept);
}
}
}

If you wonder how to intercept all touch events in DialogFragments, here you go:
abstract class BaseDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : Dialog(requireContext()){
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
// do your thing here
return super.dispatchTouchEvent(ev)
}
}
}
}

Related

Motion Event - press duration

How can I get a hold time for an item? I would like to show dialog not on every touch but on a continuous touch for example 2s. Something similar to Long Click.
My listener:
holder.itemView.setOnTouchListener(object: View.OnTouchListener{
val fragment = PodgladFragment()
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when(event?.action){
MotionEvent.ACTION_DOWN -> {
fragment.show((context as FragmentActivity).supportFragmentManager, "Dialog")
}
MotionEvent.ACTION_UP -> {
if(fragment.isAdded)
fragment.dismiss()
}
}
return true
}
})

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)
}

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

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?

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 }

ScrollView inside a RecyclerView android

I am currently working with recyclerView and cards.
I have a cards layout. Now i want to add scrollview inside the cards is that possible. If yes then how ?
The brown layout is the recycler view layout while the blue layout is the cards layout. Inside each card i need a scrollview is that possible ?
I am not sure if this will work but you can try this
Disable the touch of recycler view when scrollview is being touched.
Similarly disable scrollview touch on recyclerview touch.
This can be acheived like this
//For scrollView
scrollView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
// Disallow the touch request for parent scroll on touch of child view
view.getParent().requestDisallowInterceptTouchEvent(false);
return true;
}
});
see if this helps you.
I finally found an another way how the ScrollView can be scrolled inside RecyclerView viewHolder. As Ankit Khare's answer, in order to this works, we should disable the touch of RecyclerView when ScrollView is being touched, but his solution is too simple and it didn't quite work well after I tried that. Yeah I know both question and answers were posted from 6 years ago, maybe at that time it worked, but now it's not work anymore after I tried that.
Here is my analysis. Instead of disable the touch of RecyclerView, we should disable its scroll mechanism instead. In order to RecyclerView's scroll can be disabled, we should use function canScrollVertically() from LayoutManager which you use for the RecyclerView. Then, in RecyclerView's adapter, I implemented onTouchListener() for the ScrollView to trigger the touch on it and disable or enable the RecyclerView. So, here is my sample code (I'm using Kotlin):
class MyActivity : AppCompatActivity(), MyAdapter.AdapterListener {
private var isVerticalScrollEnabled = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/* Set RecyclerView adapter here */
recyclerView.apply {
layoutManager = object : LinearLayoutManager(this#MyActivity) {
override fun canScrollVertically(): Boolean {
return isVerticalScrollEnabled
}
}
adapter = MyAdapter(callback = this#MyActivity)
}
}
/**
* Callback from MyAdapter.AdapterListener.
*/
override fun onScrollViewTouched(isTouched: Boolean) {
isVerticalScrollEnabled = !isTouched
}
}
class MyAdapter internal constructor(private val callback: AdapterListener) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, i: Int) {
/* set touch listener for scrollview */
scrollView.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
return when (event?.action) {
MotionEvent.ACTION_DOWN -> {
callback.onScrollViewTouched(isTouched = true)
true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
callback.onScrollViewTouched(isTouched = false)
true
}
else -> {
/* return false so that scrollview can scroll the content */
false
}
}
}
})
}
internal interface AdapterListener {
fun onScrollViewTouched(isTouched: Boolean)
}
}
Hope this will help on someone who look up another answer.
Try this instead.
public class NoInterceptScrollView extends ScrollView {
public NoInterceptScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
}

Categories

Resources