'How to make NavigationView header sticky

Is it possible to make the header sticky in the design support library NavigationView?

<android.support.design.widget.NavigationView
    android:id="@+id/nav_drawer"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    app:headerLayout="@layout/nav_header"
    app:menu="@menu/nav_drawer"
    style="@style/navigation_view_drawer"
    />

EDIT:

My attempts so far have led to this

Overriding the NavigationView widget and adding a new method:

public class CustomNavigationView extends NavigationView {

public CustomNavigationView(Context context) {
    super(context);
}


public CustomNavigationView(Context context, AttributeSet attrs) {
    super(context, attrs);
}


public CustomNavigationView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
}


// Inflates header as a child of NavigationView, on top of the normal menu
public void createHeader(int res) {
    LayoutInflater inflater = LayoutInflater.from(getContext());;
    View view = inflater.inflate(res, this, false);
    addView(view);
}

}

Then adding this to the onCreate of the activity:

CustomNavigationView navigationView = (CustomNavigationView) findViewById(R.id.your_navigation_view);
navigationView.createHeader(R.layout.your_header);

This achieves the desired effect (if a bit hackily) but when menu items are below the header you can still click them, any ideas to fix this?



Solution 1:[1]

After lots of experimenting I've got something that works...it's very hacky and I'd love to hear any suggestions you have to improve it!

The overrided NavigationView

public class CustomNavigationView extends NavigationView {

public CustomNavigationView(Context context) {
    super(context);
}


public CustomNavigationView(Context context, AttributeSet attrs) {
    super(context, attrs);
}


public CustomNavigationView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
}


// Consumes touch in the NavigationView so it doesn't propagate to views below
public boolean onTouchEvent (MotionEvent me) {
    return true;
}


// Inflates header as a child of NavigationView
public void createHeader(int res) {
    LayoutInflater inflater = LayoutInflater.from(getContext());
    View view = inflater.inflate(res, this, false);

    // Consumes touch in the header so it doesn't propagate to menu items below
    view.setOnTouchListener(new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event){
            return true;
        }
    });

    addView(view);
}


// Positions and sizes the menu view
public void sizeMenu(View view) {
    // Height of header
    int header_height = (int) getResources().getDimension(R.dimen.nav_header_height);

    // Gets required display metrics
    DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
    float screen_height = displayMetrics.heightPixels;

    // Height of menu
    int menu_height = (int) (screen_height - header_height);

    // Layout params for menu
    LayoutParams params = new LayoutParams(
            LayoutParams.MATCH_PARENT,
            LayoutParams.WRAP_CONTENT);
    params.gravity = Gravity.BOTTOM;
    params.height = menu_height;
    view.setLayoutParams(params);
}

}

And this in the onCreate of main activity

// Inflates the nav drawer
    CustomNavigationView navigationView = (CustomNavigationView) findViewById(R.id.your_nav_view);
    navigationView.createHeader(R.layout.your_nav_header);

    // sizes nav drawer menu so it appears under header
    ViewGroup parent = (ViewGroup) navigationView;
    View view = parent.getChildAt(0);
    navigationView.sizeMenu(view);

Solution 2:[2]

I know the question is old, but Maybe I can help to someone else.

<android.support.design.widget.NavigationView
    android:id="@+id/nav_drawer"
    style="@style/navigation_view_drawer"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    app:headerLayout="@layout/nav_header"
    app:menu="@menu/nav_drawer">

    <include layout="@layout/nav_header" />
</android.support.design.widget.NavigationView>

app:headerLayout is redundant but is required.

Solution 3:[3]

Edited from enyciaa's answer.
This is simplified, more stable and with support of Android databinding.
Also, added an API to get the sticky header view (in case of databinding to the header layout)

The overrided NavigationView

// Add this annotation if databinding is enabled
@BindingMethods({
    @BindingMethod(type = StickyHeaderNavigationView.class, attribute = "stickyHeader", method = "setStickyHeader")
})
public class StickyHeaderNavigationView extends NavigationView {

@Nullable
View mStickyHeaderView;

public StickyHeaderNavigationView(Context context) {
    super(context);
}

public StickyHeaderNavigationView(Context context, AttributeSet attrs) {
    super(context, attrs);
}

public StickyHeaderNavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

// Consumes touch in the NavigationView so it doesn't propagate to views below
@Override
public boolean onTouchEvent(MotionEvent me) {
    return true;
}

@Nullable
public View getStickyHeaderView() {
    return mStickyHeaderView;
}

// Set a header layout as a child of NavigationView
public void setStickyHeader(@LayoutRes int res) {

    if (mStickyHeaderView != null) {
        removeView(mStickyHeaderView);
    }

    LayoutInflater inflater = LayoutInflater.from(getContext());
    View view = mStickyHeaderView = inflater.inflate(res, this, false);

    // Consumes touch in the header so it doesn't propagate to menu items below
    view.setOnTouchListener(new OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            return true;
        }
    });

    addView(view);

    // Listen to layout update. When ready, we can do #sizeMenu
    getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener(this, view));
}

// Positions and sizes the menu view
private void sizeMenu(@NonNull View view, View headerView) { 

    // Height of header
    int headerHeight = headerView.getHeight();

    // Gets required display metrics
    DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
    float screenHeight = displayMetrics.heightPixels;

    // Height of menu
    int menuHeight = (int) (screenHeight - headerHeight);

    // Layout params for menu
    LayoutParams params = new LayoutParams(
            LayoutParams.MATCH_PARENT,
            LayoutParams.WRAP_CONTENT);
    params.gravity = Gravity.BOTTOM;
    params.height = menuHeight;
    view.setLayoutParams(params);
}


// To listen for layout update, that means header height was calculated
private static class OnGlobalLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener {

    final WeakReference<StickyHeaderNavigationView> mViewRef;
    final WeakReference<View> mHeaderView;

    OnGlobalLayoutListener(@NonNull StickyHeaderNavigationView view,
                           @NonNull View headerView) {
        mViewRef = new WeakReference<>(view);
        mHeaderView = new WeakReference<>(headerView);
    }

    @Override
    public void onGlobalLayout() {

        StickyHeaderNavigationView view = mViewRef.get();
        if (view == null) {
            return;
        }

        // Update once only as the header should be fixed size
        view.getViewTreeObserver().removeOnGlobalLayoutListener(this);

        View headerView = mHeaderView.get();
        if (headerView == null) {
            return;
        }

        // childAt(0) is the navigation menu
        view.sizeMenu(view.getChildAt(0), headerView);
    }
}
}

For databinding enabled
Apply this in your layout xml

<StickyHeaderNavigationView
    ...
    app:stickyHeader="@{@layout/your_nav_header}" />

For databinding not enabled
Apply this in the onCreate of main activity

// Inflates the nav drawer
CustomNavigationView navigationView = (CustomNavigationView) findViewById(R.id.your_nav_view);
navigationView.setStickyHeader(R.layout.your_nav_header);

Solution 4:[4]

From @crash answer

xml

<android.support.design.widget.NavigationView
    android:id="@+id/nav_drawer"
    style="@style/navigation_view_drawer"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    app:headerLayout="@layout/nav_header"
    app:menu="@menu/nav_drawer">

    <include layout="@layout/nav_header" />
</android.support.design.widget.NavigationView>

Java

/* Access to a nav_header view */
View view = navigationView.getRootView().findViewById(R.id.view_id);

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Crash
Solution 3
Solution 4 le Mandarin