Android Navigation Drawer Doesn't Pass onTouch

2019-02-09 04:46发布

I have an Activity which uses the Android NavigationDrawer. When using only fragments (as usual), everything works perfect. But now I want to use this drawer on other activities of my app, and for some of them, I don't want the main view to be a fragment.

Question
The problem is, the onTouchEvent() of the activity itself (and the onItemClickedListener() of a child ListView for that matter) isn't called, because the drawer consumes it. Of course, I want it to be called:)
Needless to say, I would hope the answer will be simple (even a XML one), and hopefully not by extending the Drawer class (unless that's what it takes of course).

More Info
The Activity's main layout is very simple, basically a ListView and the DrawerLayout on top of it (below in XML).
The Drawer has one fragment as it's childView (for fragment navigation) and of course, the ListView for the Drawer Items.

I've seen many questions regarding (not exactly) similar issues, and the frequent answer was to use onInterceptTouch(), requestDisallowInterceptTouchEvent() on the DrawerLayout, and on the Parent view (Activity's main content) and even onTouchEvent() (with False returned) on the ListView of the Drawer.
Nothing seems to do the trick.

I read this link
and it does seem like using Intercept methods somewhere could be the answer. But how?

Please let me know if you need any code. But it's a very basic code/layout for this matter.
Thanks!

4条回答
Lonely孤独者°
2楼-- · 2019-02-09 05:15

Apparently the answer is somewhat easy, although it does make you extend the DrawerLayout and do some thinking, and maybe will result in some strange results (using the LAST example, I haven't seen any, yet).

Anyway, related questions which looking backwards can help understanding the issue (will explain about the first one later on):
1. DrawerLayout prevents call of MainActivity.onTouchEvent()
2. How can I requestDisallowTouchEvents on Android DrawerLayout
3. Set drag margin for Android Navigation Drawer

Answer
First, please note that I put lots of examples here. If you just want the best one (for me), jump to the last one.
Secondly, if someone has enough reputation, please comment on the first link's question and put a link to this answer (it can help that guy).

Example 1
Well, basically, just extend Android's DrawerLayout and replace onTouchEvent() to this:

@Override
public boolean onTouchEvent(MotionEvent arg0) {
    super.onTouchEvent(arg0);
    return false;
}

This solution will do anything except that it won't open the Drawer on slides, only menu clicks and the like. Besides, it forwards clicks so when the Drawer is open for instance, touching outside of it will NOT close it, but click on whatever is behind (e.g. a ListView). Le'ts try harder...

Example 2
Now, let's catch the open OR visible cases, to return true (and consume the action at the Drawer).

@Override
public boolean onTouchEvent(MotionEvent arg0) {
    super.onTouchEvent(arg0);

    if(isDrawerOpen(findViewById(R.id.list_slidermenu)) || 
            isDrawerVisible(findViewById(R.id.list_slidermenu))){
        return true;
    }

    return false;
}

This solution is better, as it prevents clicks on behind the Drawer when the drawer is open or even visible (slide starts...). But touch-sliding it still doesn't work.

Example 3
Ok, so let's just split cases. Touches (MotionEvent.ACTION_DOWN) inside the Drawer's margin (area that Google desided to slide Drawer when touched at) will result in returning True to consume the action, and others will forward the event (return False).

@Override
public boolean onTouchEvent(MotionEvent arg0) {
    super.onTouchEvent(arg0);
    float edge = 30;//that's for a left drawer obviously. Use <parentWidth - 30> for the right one.
    View mDrawerListView = findViewById(R.id.drawer_listview);

    if(isDrawerOpen(mDrawerListView) || 
            isDrawerVisible(mDrawerListView)){
        return true;
    } else if(arg0.getAction() == MotionEvent.ACTION_DOWN && arg0.getX() > edge){
        return false;
    }

    return true;
}

Note that I used 30dp. That's what I found to be the margin (although in one of the links it is said to be 20....).

Well, the next example would of course be deciding what is, exactly, that edge (see in code above) value is, according to Android. We don't want to use a number that could change or whatever.

New Question
So now that first link should come handy. It "hacks" the Drawer code to get that Drawer edge/megin number. BUT, it didn't work for me, as those exact Field names could not be found.
I run mDrawerLayout.getClass().getField() which returns all the fields, but without any luck finding what we want. Anyone?

Last Example - Full Code
Ok, looking on example number 3, after understanding what exactly I did, we can make it faster by extending the onFinishInflate() method and save it as a global variable for this CustomDrawerLayout for later use. We can also put that first 'if' inside the second one to save some more work. OK here goes:

View mDrawerListView;
...

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    mDrawerListView = findViewById(R.id.drawer_listview);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    super.onTouchEvent(event);

    if(event.getX() > 30 && event.getAction() == MotionEvent.ACTION_DOWN){
        if(isDrawerOpen(mDrawerListView) || isDrawerVisible(mDrawerListView)){
            return true;
        } else{
            return false;
        }
    }

    return true;
}

That's it for now! Hope it'll helps someone in the future beside myself, hehe....

查看更多
兄弟一词,经得起流年.
3楼-- · 2019-02-09 05:22

I have a solution:

Set OnTouchListener on the screen layout (the first childview of DrawerLayout, normally) and transmit the TouchEvent to a custom GestureDetector.

So, you can do your own things in it. One more important thing: if you want to override onSingleTapUp() or something else, you should return true in onDown() to make sure that you can get the rest MotionEvent to make onSingleTapUp() work.

private class MyGestureListener implements GestureDetector.OnGestureListener{
    @Override
    public boolean onDown(MotionEvent e) {

        return true;
    }

    @Override
    public void onShowPress(MotionEvent e) {

    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        // do your own things
        return true;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {

    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }


}

and set it :

 mGestureDetector=new GestureDetector(this, new MyGestureListener());

    layout_content.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {

            return  mGestureDetector.onTouchEvent(event);
        }
    });
查看更多
冷血范
4楼-- · 2019-02-09 05:35

To add on to guy_m 's answer, here is my implementation for a drawer that opens from the right, includes constructors so that it is viewable in the layout editor and also takes into account when a user swipes from past the edge point:

public class CustomDrawerLayout extends DrawerLayout {
View mDrawerListView;
float edge;
int holddown = 0;
static final String TAG = CustomDrawerLayout.class.getSimpleName();

public CustomDrawerLayout(@NonNull Context context) {
    super(context);
    setscreendimensionvals(context);
}

public CustomDrawerLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    setscreendimensionvals(context);
}

public CustomDrawerLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    setscreendimensionvals(context);
}

private void setscreendimensionvals(Context context){
    DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
    /*((Activity) context).getWindowManager()
            .getDefaultDisplay()
            .getMetrics(displayMetrics); */
    int width = displayMetrics.widthPixels;
    float density = displayMetrics.density;
    edge = width - (30 * density); // 30 is the edge of the screen where the navigation drawer comes out
    Log.d(TAG,"edge: " + edge);
    Log.d(TAG,"width: " + width);
}
@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    mDrawerListView = findViewById(R.id.drawerconstraint_overworld);
}

@Override
public boolean onTouchEvent(MotionEvent event){
    super.onTouchEvent(event); // need to add action up and a local variable to detect when lifted finger
    //Log.d(TAG,"point: " + event.getX());
    if(event.getX() >= edge && (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE)){
        holddown = 1;
        //Log.d(TAG,"hold down");
    }

    if(event.getAction() == MotionEvent.ACTION_UP){
        holddown = 0;
        //Log.d(TAG,"hold up");
    }

    if(holddown == 1){
        return  true;
    }else{
        if(isDrawerOpen(mDrawerListView) || isDrawerVisible(mDrawerListView)){
            return true;
        } else{
            return false;
        }
    }
}

}

查看更多
家丑人穷心不美
5楼-- · 2019-02-09 05:36

While working on the same problem I was inspired by guy_m's answer and boiled down his proposals to the following solution.

Again it amounts to extending DrawerLayout and overriding onInterceptTouchEvent(). The logic is simple:

  • Whenever the touch event occurs off the drawer view (the slideable part), we return false. Then our DrawerLayout is out of the game when it comes to handling the event -- the event is handled by whatever view we put into the DrawerLayout at the respective position.

  • On the other hand, when the event occurs inside the drawer view, we delegate to super.onInterceptTouchEvent() to decide what to do with the event. That way the drawer will slide in and out as before on touch gestures happening on itself.

The following code sample is for a DrawerLayout whose drawer view is located on the right (android:gravity="right"). It should be obvious how to modify it to cover also the case of a left-placed drawer.

public class CustomDrawerLayout extends DrawerLayout
{
  @Override
  public boolean onInterceptTouchEvent( MotionEvent event )
  {
    final View drawerView = getChildAt( 1 );
    final ViewConfiguration config = ViewConfiguration.get( getContext() );
    // Calculate the area on the right border of the screen on which
    // the DrawerLayout should *always* intercept touch events.
    // In case the drawer is closed, we still want the DrawerLayout
    // to respond to touch/drag gestures there and reopen the drawer!
    final int rightBoundary = getWidth() - 2 * config.getScaledTouchSlop();

    // If the drawer is opened and the event happened
    // on its surface, or if the event happened on the
    // right border of the layout, then we let DrawerLayout
    // decide if it wants to intercept (and properly handle)
    // the event.
    // Otherwise we disallow DrawerLayout to intercept (return false),
    // thereby letting its child views handle the event.
    return ( isDrawerOpen( drawerView ) && drawerView.getLeft() <= event.getX()
            || rightBoundary <= event.getX() )
            && super.onInterceptTouchEvent( event );
  }
}
查看更多
登录 后发表回答