smooth-app-bar-layout
smooth-app-bar-layout copied to clipboard
I cannot initiate a scroll from a clickable view inside SmoothAppBarLayout
I understand this was already reported as issue #54. I'm creating a new issue as that one was closed.
I need to have a clickable ImageView inside a CollapsingToolbarLayout inside the SmoothAppBarLayout. And as long as the ImageView has a ClickListener the scrolling doesn't work starting the scroll on that view.
It's critical in my case as the ImageView is as big as the entire CollapsingToolbarLayout and contains the first item image and you can tap on it to transition to the all-images dedicated screen.
I reproduced this by adding a clickable view inside the CollapsingToolbarLayout of your samples project screens GsdScrollExitUntilCollapsedActivity and SmoothScrollExitUntilCollapsedActivity to confirm the problem is in SmoothAppBarLayout. This is not a from-scratch implenentation; I'm coming from a:
RelativeLayout
CoordinatorLayout
AppBarLayout
CollapsingToolbarLayout
RelativeLayout
ImageView (clickable)
~other stuff floating around
Toolbar
NestedScrollView
LinearLayout
~item details
I took a look and compared the onTouch events of the clickable view on each of those examples while starting a scroll and noticed that in the GsdScrollExitUntilCollapsedActivity case it receives the ACTION_DOWN... then a few ACTION_MOVE... then an ACTION_CANCEL... and then the further ACTION_MOVE events start to get detected on the parent views.
In the SmoothScrollExitUntilCollapsedActivity case the ACTION_CANCEL is never received by the clickable view and the ACTION_MOVE events never get received by it's parents.
On the other issue both you and the reporting guy mentioned something about "dispatching the touch events on my own" but I need a bit more help about how to do that.... should I be detecting the ACTION_MOVE onTouch events (I already have this) and dispatching them (using the dispatchTouchEvent function ?) but... to who ? to the CoordinatorLayout ?
Let me try it again. What SmoothAppBar version are you using?
Hi @pablo-oses
Can you change the order in your layout, then see how it works?
RelativeLayout
CoordinatorLayout
NestedScrollView
LinearLayout
~item details
AppBarLayout
CollapsingToolbarLayout
RelativeLayout
ImageView (clickable)
~other stuff floating around
Toolbar
In my app I'm using 23.1.0 for both the appcompat and smooth-app-bar-layout The layout structure that I mentioned was before implementing SmoothAppBarLayout. In the current one I have NestedScrollView first (with a top padding) and SmoothAppBarLayout later.
Instead of trying to fix this in my app with all it's complexities I'm trying to confirm that a possible fix is doable in the samples app as an in-vitro experiment.
I'm using the master branch as of last week (the last thing mentioned in the CHANGES.MD is "Version 23.2.1.1 - March 29th 2016"
In the sample GsdScrollExitUntilCollapsedActivity the clickable view inside CollapsingToolbarLayout inside AppBarLayout receives the onTouch(ACTION_CANCEL) event after a few onTouch(ACTION_MOVE) events. And in the sample SmoothScrollExitUntilCollapsedActivity it doesn't.
So to try to understand why it's not working I started to research exactly how is it working fine on the GsdScrollExitUntilCollapsedActivity case.
So I started to read about the Android's touch system event propagation and understood that such event (the ACTION_CANCEL) is received because some parent view answered true to a ViewGroup.onInterceptTouchEvent event. So I started to sniff the onInterceptTouchEvent activity of each of the parent views and noticed that CoordinatorLayout is the one detecting a true against ViewGroup.onInterceptTouchEvent and stops sending the ACTION_MOVE events to the clickable view and starts to process them on his own. The one telling CoordinatorLayout that true is the AppBarLayout.Behavior. The detection logic is in it's base class HeaderBehavior and is clear in the case of ACTION_MOVE if it detects yDiff > mTouchSlop
And when I analysed all that same logic again for the SmoothScrollExitUntilCollapsedActivity case I noticed that I'm in SmoothAppBarLayout.Behaviour which also extends the same HeaderBehavior so the detection logic should work in the same way but when action == ACTION_MOVE it fails to work because mActivePointerId is -1 (which should have been set in the ACTION_DOWN event.
When the HeaderBehavior of SmoothAppBarLayout received the ACTION_DOWN event it didn't saved the mActivePointerId because canDragView(child) is false (a function that in the case of AppBarLayout.Behavior answers true)
And the reason for that difference is that SmoothAppBarLayout.Behavior extends BaseBehavior before extending AppBarLayout.Behavior and in BaseBehavior's init function it set's a DragCallback that always return false.
I commented out that part and noticed that the ACTION_MOVE events are now properly transferred from the clickable view to the CoordinatorLayout as the case in which it works. But now while I initiate a scroll from SmoothAppBarLayout the other members of the CoordinatorLayout (in your sample a RecyclerView) doesn't scroll :(
Do you have any idea to have a clickable view inside SmoothAppBarLayout ?
Hi @pablo-oses
What AppCompat version are you using? The latest version of SmoothAppBarLayout is 23.3.0
. Please check if it works for you.
Cheers, Henry
I upgraded to 23.4.0
on the sample/build.gradle file and it's still not scrolling from a clickable view inside the SmoothAppBarLayout. (the touch events never get intercepted and captured by it's parent views)
Just want to let you know that I am looking into this issue. It's a common problem with custom view, ViewGroup and View... Somehow, original AppBarLayout works. Investigating...
Thanks !... I hope all the debugging findings that I wrote helps. Let me know if you can't reproduce in the samples app anything that I described or if I can help trying out possible ideas
I am just back from I/O and will ping you when I have some updates. Thanks for your attention.
I still haven't had perfect solution for this issue yet. This is tricky issue. I need to spend more time look into event dispatcher things...
Same issue here. I Have a button inside my CollapsingToolbarLayout
but scrolling is disabled.
I am facing a similar issue too. I have a ViewPager inside the SmoothCollapsingToolbarLayout and ViewPager items are clickable.
I also encountered the same problem
I am also facing this issue, tested with latest lib
With android AppBarLayout they have fixed this issue Does this issue has something to do with onTouchListener of clickable view
Right, because of the approach of SmoothAppBarLayout, I still can't figure out the way to make touch and scroll work at the same time. :(
I am still new to Application development so i dont know the right approch , but is is possible to detect touch event inside SmoothAppBarLayout or any parent view and check for action type and separate down action from move action if it is move then initiate SmoothAppBarLayout scroll event , else do nothing
https://developer.android.com/training/gestures/detector.html http://stackoverflow.com/questions/14776271/android-ontouch-motionevent-action-move-is-not-recognized
please let me know is this possibly right approach or not
can you try this on your side
I also encountered the same problem for: SmoothAppBarLayout---->CollapsingToolbarLayout----->ViewPager.
Thanks @nikhilreprime for your suggestion. It can be a right approach but it's still not an easy fix. Thanks @anilbisht for reporting this issue too. I will try my best to support scrolling on clickable elements.
@henrytao-me Hey buddy, Any update on this ? I am also facing the same issue. Please help me out.
@henrytao-me Hey buddy, Any update on this ? I am also facing the same issue. Please help me out too ,thank you!.
@henrytao-me I found a workaround for the issue, which is nowhere near optimum, but for my requirements it works fine and it could be a start for a solution.
First one would need to return false for all dispatchTouchEvents in SmoothAppBarLayout, so the behaviour can take over the touches.
@CoordinatorLayout.DefaultBehavior(SmoothAppBarLayout.Behavior.class)
public class SmoothAppBarLayout extends AppBarLayout {
// ...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
super.dispatchTouchEvent(ev);
return false;
}
// ...
}
Secondly, we will need to pass down the touch events, so all clicklisteners in the header etc. will receive their events, which unfortunately requires to override CoordinatorLayout
public class SmoothCoordinatorLayout extends CoordinatorLayout {
private SmoothAppBarLayout vAppBarLayout;
// ...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = super.dispatchTouchEvent(ev);
if (handled && vAppBarLayout != null) {
vAppBarLayout.dispatchTouchEvent(ev);
}
return handled;
}
@Override
public void onLayoutChild(View child, int layoutDirection) {
super.onLayoutChild(child, layoutDirection);
if (child instanceof SmoothAppBarLayout) {
vAppBarLayout = (SmoothAppBarLayout) child;
}
}
// ...
}
This is far from optimum, but in my case it works fine.
@florianPOLARSTEPS that part didn't improved anything in my code. In fact, it has even disabled my touch events
Thanks for sharing. I will look into it shortly
@henrytao-me Due to the nature of the workaround ( basically duplicating touch events ) there are a few side effects. It can happen that 2 touches at the same time are registered.
I am also looking for a proper solution, but I am still trying to wrap my head around the whole touch event propagation system in a coordinator layout.
@henrytao-me I found another possible workaround which is a bit nicer since it does not require a custom CoordinatorLayout and will work for clicks in the header only.
@CoordinatorLayout.DefaultBehavior(SmoothAppBarLayout.Behavior.class)
public class SmoothAppBarLayout extends AppBarLayout {
private static final int CUSTOM_EDGE_FLAG = 2023477;
// ...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// We need to check wether we have arrived from our own triggered motion dispatch,
// There are no really appropriate fields on MotionEvents to store custom data, so we abuse edgeflags
if (ev.getEdgeFlags() == CUSTOM_EDGE_FLAG) {
return false;
}
boolean dispatched = super.dispatchTouchEvent(ev);
if (dispatched && ev.getAction() == MotionEvent.ACTION_MOVE) {
// After we know some view in our hierarchy would want to receive the move touch event, we don't want it to have though,
// we create a new motion event which will cancel our current motion event stream and will be disregarded by appbarlayout,
// so CoordinatorLayout.Behaviour can receive the new motion event stream
MotionEvent motionEvent = MotionEvent.obtain(ev);
motionEvent.offsetLocation(getLeft(), getTop());
motionEvent.setAction(MotionEvent.ACTION_DOWN);
motionEvent.setEdgeFlags(CUSTOM_EDGE_FLAG);
// getParent() cannot return null, since well - who would have called this method
((ViewGroup) getParent()).dispatchTouchEvent(motionEvent);
return false;
}
return dispatched;
}
// ...
}
Hi @florianPOLARSTEPS, it really works. Awesome. Let's me add that to next release and mark you as contribution. Thanks a lot.
@florianPOLARSTEPS thanks for providing this workaround @henrytao-me when are you planning to deploy next release
The work around works fine in case of non scrolling elements in header In my case horizontal scroll view is present in header view So i added few extra line , please check if they are OK Thank you
private float downXValue,downYValue;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getEdgeFlags() == CUSTOM_EDGE_FLAG) {
return false;
}
boolean dispatched = super.dispatchTouchEvent(ev);
if(ev.getAction() == MotionEvent.ACTION_DOWN) {
downXValue = ev.getX();
downYValue = ev.getY();
}else{
float currentX = ev.getX();
float currentY = ev.getY();
if (dispatched && ev.getAction() == MotionEvent.ACTION_MOVE) {
if (Math.abs(downXValue - currentX) > Math.abs(downYValue - currentY)) {
Log.d("Motion Type :","Horizantal");
}else{
Log.d("Motion Type :","Vertical");
MotionEvent motionEvent = MotionEvent.obtain(ev);
motionEvent.offsetLocation(getLeft(), getTop());
motionEvent.setAction(MotionEvent.ACTION_DOWN);
motionEvent.setEdgeFlags(CUSTOM_EDGE_FLAG);
// getParent() cannot return null, since well - who would have called this method
((ViewGroup) getParent()).dispatchTouchEvent(motionEvent);
return false;
}
}
}
return dispatched;
}
Thanks @nikhilreprime for the workaround. I think I will include it to next release by early next month when I have more spare time and add you guys to contributors list.
I heard that the fix for AppBarLayout issue is done. It will be included in 26.0.0. Stay tuned.