MotionLayout problems with children intercepting touch events
Asked Answered
P

2

10

I have a motionLayout for the root container, in my main layout. Inside it, there are other views. One of them is a frameLayout, containing a fragment. The fragment is a page, consisting a NestedScrollView etc... MotionLayout has OnSwipe for only horizontal swipes, and the NestedScrollView should only be able to be scrolled vertically. I had to extend the MotionLayout onInterceptTouchEvent method (look at the code below) and only pass to children those touch events which were not horizontal. Pretty straightforward so far and it works.

The problem is, when I start swiping on views which absorb some kind of touch events (like buttons, or the NestedScrollView) it messes up the motionLayout transitions and it skips frames instantaneously, so the anchor corresponds with my mouse position. The transition progresses almost completely in a frame and kills the UI experience. My guess is, if the motionLayout takes X1 and X2 for the transitions, the X1 value is the one casuing the problem and its value coming from where it's not supposed to. But of course when I start swiping on touch absorbing elements.

This is the override method of OnInterceptTouchEvent of my customMotionLayout:

 override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        super.onInterceptTouchEvent(event)

        return when (event.actionMasked) {
            MotionEvent.ACTION_DOWN -> {
                previousX = event.x.toInt()
                previousY = event.y.toInt()
                mIsScrolling = false
                false
            }
            MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
                mIsScrolling = false
                false
            }
            MotionEvent.ACTION_MOVE -> {
                if (mIsScrolling) true
                 else {
                    var xMotion: Int = abs(previousX - event.x.toInt())
                    var yMotion: Int = abs(previousY - event.y.toInt())
                    previousX = event.x.toInt()
                    previousY = event.y.toInt()
                    if (xMotion >= yMotion) {
                        mIsScrolling = true
                        true
                    } else false}}
            else -> {
                mIsScrolling = false
                false
            }
        }
    }

This is the relevant transition:

    <Transition android:id="@+id/options_to_now" motion:motionInterpolator="easeInOut"
        motion:pathMotionArc="none"
        motion:constraintSetStart ="@id/options_state"
        motion:constraintSetEnd="@id/now_state"
        motion:duration="100">
        <OnSwipe
            motion:maxAcceleration="100"
            motion:maxVelocity="100"
            motion:dragDirection="dragRight"
            motion:touchAnchorId="@id/view_options"
            motion:touchAnchorSide="left"
            motion:touchRegionId="@id/view_options"/>
    </Transition>

I made sure with testing that there is no problem with MotionDescripton and layouts.

It's noteworthy that the motion works perfectly when I don't touch on elements which absorb some kind of touch events. Like blank spaces in my layout, and it only causes mentioned problem when I swipe over those elements.

Maybe If I knew how motionLayout transitions are related to touchPositions, I could fix it.

UPDATE 1: I noticed if I click on a blank space (simple click ACTION_DOWN), and then swipe, even starting on those naughty elements, the problem differs. That click somehow updates the coordination positions of the transition. It takes where I click as the starting point, for example x1. Then when I swipe over something that absorbs touch events, it gets its x2 from there. Now what I'm seeing in the exact next frame is like the result as if I swiped the motion, from x1 to x2.

UPDATE 2: Another thing I've noticed which I think is pretty much related to my problem, is that the transition always is in the STARTED state, when I'm done swiping. Like I swipe from x1 to x2, its states goes from started, completed and then started. So I guess when my next swipe appears, it thinks my fingers were on the touch all the time and I manually siwped, which I didn't. The result in the log after listening to the transition and prining the states are as follows, when I do a complete cycle of swiping geture and finishig it while there is no fingers on the screen. So the last event that is called is started, which is not logical to me.

D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.014359029
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.02863888
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.044761233
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.06311959
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.09285617
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.12685902
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.17269236
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.22314182
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.27269235
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.32545927
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.389359
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.4449254
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.44595188
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.4948284
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.57895637
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.6884479
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.814522
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.8918733
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.95612586
D/e: Changed, state = -1 / p1 = 2131230901 / p2 = 2131230907 / p3 = 0.9949746
D/e: Completed: state = 2131230907 / p1 = 2131230907
D/e: Started, state = 2131230907 / p1 = 2131230901 / p2 = 2131230907
    Changed, state = 2131230907 / p1 = 2131230901 / p2 = 2131230907 / p3 = 1.0

UPDATE 3: Another thing I've noticed is that, if and only if a transition successfully changes state of the motion, we come to a situation where by clicking on one of those touch absorbing elements, motionLayout doesn't recieve OnTouchEvent! (and this is the situation, in where the problematic behaviour I'm dealing with exist) and If I don't swipe anything when the application starts (while there is no problem yet), by clicking on a button, I recieve ACTION_DOWN and ACTINO_UP in the motionLayout. So whatever happens in the motionLayout after a transition to a new state, it prevents the motionLayout to recieve OnTouchEvents from the children.

UPDATE 4: I'm not very familiar with how MotionLayout handles scene creation, but if someone knows, could it be that motionLayout creates a scene dynamicaly which somehow prevents onTouchEvent travels all the way back up to the parents?

UPDATE 5: So when the motionEvent with ACTION_DOWN is not being recieved by the motionLayout, the problem arises. I've looked the call stack and figured it should be related to the view absorbing event dispatchTouchEvent method, returning true for the ACTION_DOWN when start swiping on touch absorbing elements, and returning false for other cases. So, returning true in dispatchTouchEvent causes the motionLayout not recieving the ACTION_DOWN and causing the problems. Inside the dispatchTouchEvent, the code that diffes the results for two situations is the part:

  if (onFilterTouchEventForSecurity(event)) {
        ...
        if (!result && onTouchEvent(event)) {
             result = true;
        }
     }

the OnTouchEvent(event) returns true when clicking on buttons for example and false when clicking on blank spaces.

I'm using 2.0.0-beta4 version of Constraint Layout.

Peckham answered 27/12, 2019 at 18:0 Comment(3)
Having exactly the same problem. Found out by accident, that when I start swiping on elements consuming touches, like recyclerview, my touch region is strangely shifted to some irrelevant position, like top right of the screen where there are no touch absorbing elements. From there the swipe works.Pasadena
@Pasadena Have you found a solution?Papiamento
Why are you using motion:touchRegionId="@id/view_options" ? Especially since you are overriding onInterceptTouchEvent. In general use limitBoundsToSprang
P
0

I guess I found the problem, it's the mRegionView in the onInterceptTouchEvent of motionLayout. Somwhere in the method, it checks the bounds of event x and y with the left and right, top and bottom of the mRegoinView. I looked at the values and I saw in those problematic situations the values are a little odd, and thus the function returns false and never calls the onTouch event of the motionLayout. Values are:

good situation x 354 y 450 top 0 left 16 bottom 1280 rigt 752

bad situation (As described in the question, when 1 transition already successfully changed state to a new one and we start dragging on a touch absorbing element)

x 300 y 450 top 0 left 750 bottom 1280 right 768

As you can see, the left has 750 which is a little werid to me and I don't know the reason. I'll probably check why it has that value and update this answer, but for now I simply removed the boundCheck in the onInterceptTouchEvent in the motionLayout, since I'm not really using spicific regions for now at least.

UPDATE: It was the touchRegion and regionId. For a same scene and element (let's say button), for the first time when you click on it, [in the touchProcess] it works fine, but if you do a transition, then it returns a wrong view in touchProcess, regionId returns another view, which was the parent of the previous transition's scene. I don't know why this happens or is it some kind of error (considering this library is still beta, understandable). So I just overrieded the touchProcess of the MotionLayout and ignored the part which checks the bounds of the TouchRegion.

Peckham answered 29/12, 2019 at 14:10 Comment(0)
T
0

I stumbled upon a similar issue, where I had a main RecyclerView that had nested RecyclerViews in it.

While searching for a solution, I found a method called setInteractionEnabled(boolean enabled) inside the MotionLayout class. Calling that method with false value resolved my issue.

It looks like that MotionLayout by default performs some logic inside such methods as onTouchEvent and onInterceptTouchEvent. The method setInteractionEnabled basically allows us to specify if we need that logic or not.

Trevelyan answered 16/2, 2021 at 1:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.