如何在Android支持设计库中为NavigationView添加页脚?

140
我该如何设置页脚和个人资料信息,使其在 NavigationView 中呈现像电子邮件导航抽屉中的收件箱一样的效果?NavigationView 的项是通过菜单资源充气的,但我不知道如何将底部项设置为菜单资源,或者如何将自定义视图设置为 NavigationView 或底部偏移量?我尝试将此 <LinearLayout...> 作为页脚视图放置,但在小屏幕上,页脚会覆盖项目,我无法滚动菜单。我尝试为 NavigationView 设置页脚填充,但页脚也会占用填充。

这在小屏幕上无法滚动:

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

    <LinearLayout...>

</android.support.design.widget.NavigationView>

不滚动

这个可以滚动,但页脚覆盖了菜单项:

<android.support.design.widget.NavigationView
    android:id="@+id/drawer"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    android:paddingBottom="96dp"
    app:headerLayout="@layout/kuona_drawer_header"
    app:menu="@menu/drawer">

    <LinearLayout...>

</android.support.design.widget.NavigationView>

在此输入图片描述

抽屉式菜单的res/menu/drawer.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <group android:checkableBehavior="single">
        <item
            android:id="@+id/action_current_list"
            android:checked="true"
            android:icon="@drawable/ic_current_list"
            android:title="@string/current_list" />
        <item
            android:id="@+id/action_manage_lists"
            android:icon="@drawable/ic_my_lists"
            android:title="@string/my_lists" />
        <item
            android:id="@+id/action_search_products"
            android:icon="@drawable/ic_search_black_24dp"
            android:title="@string/search_products" />
        <item
            android:id="@+id/action_deals"
            android:icon="@drawable/ic_product_promo"
            android:title="@string/deals" />
    </group>
</menu>

你把@menu/drawer文件放在哪个位置了? - Psypher
它在 /res/menu/drawer.xml 中。 - epool
你不能把“反馈”和“退出”作为菜单项吗?如果原因是你想动态更改“退出”菜单图标,那么你应该可以使用menuItem.setIcon(Drawable)来实现。 - hungryghost
这只是为了满足需求,个人想知道例如 Google 在 Inbox 应用中是如何实现的。 - epool
这个问题在23.1更新后变得更加重要。 - Alex Sorokoletov
27个回答

1
这里有一个解决方案,不需要嵌套,可以根据页脚视图的高度动态调整内部NavigationMenuView的底部填充。这意味着页脚高度可以设置为wrap_content,并且可以动态更改页脚的可见性为VISIBLEGONE
public class NavigationMenuFooterView extends LinearLayout {

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

    public NavigationMenuFooterView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public NavigationMenuFooterView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
        update(getHeight());
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        update(h);
    }

    private void update(int height) {

        ViewParent parent = getParent();
        if (parent instanceof ViewGroup) {
            View navigationMenuView = ((ViewGroup) parent).findViewById(R.id.design_navigation_view);
            if (navigationMenuView != null) {
                if (getVisibility() == View.GONE) {
                    height = 0;
                }
                navigationMenuView.setPadding(navigationMenuView.getPaddingLeft(),
                        navigationMenuView.getPaddingTop(), navigationMenuView.getPaddingRight(), height);
            }
        }
    }
}

您需要在ids.xml中定义design_navigation_view的id:

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <item name="design_navigation_view" type="id"/>
    </resources>

并像这样使用:
    <com.google.android.material.navigation.NavigationView
        android:id="@+id/navigation_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        app:headerLayout="@layout/drawer_header"
        app:itemBackground="@drawable/drawer_item"
        app:itemIconTint="@color/drawer_item"
        app:itemTextColor="@color/drawer_item"
        app:menu="@menu/menu_drawer">

        <util.NavigationMenuFooterView
            android:id="@+id/navigation_footer_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"
            android:background="#fff"
            android:orientation="vertical">

            <View
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="#ddd" />

            <include layout="@layout/..." />

        </util.NavigationMenuFooterView>

    </com.google.android.material.navigation.NavigationView>

已使用com.google.android.material:material:1.0.0进行测试。


这是一个非常好的解决方案。我使用了actionLayout解决方案,在menu.xml文件中,页脚也在其中,但我已经将背景应用于列表项,所以背景也适用于页脚布局。以这种方式应用页脚将不会有那种问题。 - cgb_pandey

1
我通过创建两个布局,一个包含页眉元素,另一个包含页脚元素,并将它们包含到NavigationView中,实现了这个功能。
    <com.google.android.material.navigation.NavigationView
    android:id="@+id/nav_view"
    android:layout_width="wrap_content"
    app:headerLayout="@layout/nav_header"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    app:itemTextColor="@color/white"
    app:menu="@menu/conversation_menu"
    android:background="@color/nav"
    android:fitsSystemWindows="true">

    <!-- Header layout -->
    <include layout="@layout/nav_header" />
    <include layout="@layout/nav_footer" />
    <!-- Scrollable conversations -->
    <androidx.core.widget.NestedScrollView
        android:id="@+id/conversationsScrollView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintTop_toBottomOf="@id/nav_view"
        app:layout_constraintBottom_toTopOf="@id/nav_footer">

        <LinearLayout
            android:id="@+id/conversationsLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
            <!-- Your conversation items added dynamically -->
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>


</com.google.android.material.navigation.NavigationView>

0

我个人解决固定页眉和页脚的方法是扩展NavigationView如下:

/**
 * Created by guness on 17.01.2018.
 */
class NavigationView : android.support.design.widget.NavigationView {

private var mHeader: View? = null
private var mFooter: View? = null
private var mMenuView: NavigationMenuView? = null

constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
    val a = TintTypedArray.obtainStyledAttributes(context, attrs,
            R.styleable.NavigationView, defStyleAttr,
            R.style.Widget_Design_NavigationView)

    if (a.hasValue(R.styleable.NavigationView_footerLayout)) {
        inflateFooterView(a.getResourceId(R.styleable.NavigationView_footerLayout, 0))
    }

    a.recycle()

    (mFooter?.layoutParams as FrameLayout.LayoutParams?)?.gravity = Gravity.BOTTOM
}

init {
    (0 until childCount)
            .map { getChildAt(it) }
            .filter { it is NavigationMenuView }
            .forEach {
                mMenuView = it as NavigationMenuView
                mMenuView!!.overScrollMode = View.OVER_SCROLL_NEVER
            }
}

override fun inflateHeaderView(@LayoutRes res: Int): View {
    mHeader = LayoutInflater.from(context).inflate(res, this, false)
    setHeaderView(mHeader!!)
    return mHeader!!
}

@Deprecated("There can only be one header", ReplaceWith("#setHeaderView(view: View)"))
override fun addHeaderView(view: View) {
    throw IllegalAccessException("Please use #setHeaderView")
}

@UiThread
fun setHeaderView(view: View) {
    removeHeaderView()
    mHeader = view
    addView(mHeader, 0)
}

@Deprecated("No need to use params", ReplaceWith("#removeHeaderView()"))
override fun removeHeaderView(view: View) {
    removeHeaderView()
}

@UiThread
fun removeHeaderView() {
    if (mHeader != null) {
        removeView(mHeader)
        mHeader = null
    }
}

@Deprecated("No need to count, it is either 1 or zero", ReplaceWith("#hasHeader()"))
override fun getHeaderCount(): Int {
    return if (mHeader == null) 0 else 1
}

@Deprecated("No need to use params", ReplaceWith("#getHeaderView()"))
override fun getHeaderView(index: Int): View? {
    return getHeaderView()
}

fun getHeaderView(): View? {
    return mHeader
}

fun hasHeader(): Boolean {
    return mHeader != null
}

fun inflateFooterView(@LayoutRes res: Int): View {
    mFooter = LayoutInflater.from(context).inflate(res, this, false)
    setFooterView(mFooter!!)
    return mFooter!!
}

@UiThread
fun setFooterView(view: View) {
    removeFooterView()
    mFooter = view
    addView(mFooter, 0)
}

@UiThread
fun removeFooterView() {
    if (mFooter != null) {
        removeView(mFooter)
        mFooter = null
    }
}

fun hasFooter(): Boolean {
    return mFooter != null
}

fun getFooterView(): View? {
    return mFooter
}

fun setOnClickListener(@IdRes res: Int, listener: View.OnClickListener) {
    mHeader?.findViewById<View>(res)?.setOnClickListener(listener)
    mFooter?.findViewById<View>(res)?.setOnClickListener(listener)
}

override fun onMeasure(widthSpec: Int, heightSpec: Int) {
    super.onMeasure(widthSpec, heightSpec)
    val headerHeight = mHeader?.measuredHeight ?: 0
    val footerHeight = mFooter?.measuredHeight ?: 0
    val params = (mMenuView?.layoutParams as ViewGroup.MarginLayoutParams?)
    var changed = false
    if (params?.topMargin != headerHeight) {
        params?.topMargin = headerHeight
        changed = true
    }
    if (params?.bottomMargin != footerHeight) {
        params?.bottomMargin = footerHeight
        changed = true
    }
    if (changed) {
        mMenuView!!.measure(widthSpec, heightSpec)
    }
}
}

NavigationView 最初创建一个 LinearLayout 作为 RecyclerView 上的第一项,并将所有内容一起滚动。这样做的想法是创建独立的页脚和页眉视图,然后使用 Gravity 将它们推到顶部和底部。稍后测量 RecyclerView 的内容会解决滚动内容的问题。

这是包含我编写的上述代码的库。 https://github.com/guness/NavigationView

好处是现在我可以像本地头文件一样在 xml 中定义页脚视图:

    app:footerLayout="@layout/nav_footer_main"
    app:headerLayout="@layout/nav_header_main"

0

对于我的情况,如果有足够的空间,我需要始终将“LogOut”项目保留在“NavigationView”的底部,或者如果不适合,则“模拟”一些滚动。

目标是在不进行大量更改的情况下实现这一目标,当然也不添加嵌套的NavigationView。

对我有效的解决方案如下:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
    <androidx.drawerlayout.widget.DrawerLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/drawer_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">

        <com.google.android.material.navigation.NavigationView
            android:id="@+id/navigation_view"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="start"
            android:clipToPadding="false"
            android:clipChildren="false"
            android:overScrollMode="never"
            android:fitsSystemWindows="true"
            android:maxWidth="300dp"
            app:headerLayout="@layout/nav_drawer_header"
            app:itemBackground="@drawable/bg_nav_drawer_item"
            app:itemHorizontalPadding="25dp"
            app:itemIconPadding="25dp"
            app:itemIconTint="@color/nav_drawer_item_icon"
            app:itemTextAppearance="@style/NavigationDrawerTextStyle"
            app:itemTextColor="@color/nav_drawer_item_text"
            app:menu="@menu/navigation_drawer_menu">

            <LinearLayout
                android:id="@+id/ll_activity_main_log_out"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="bottom"
                android:background="?attr/selectableItemBackground"
                android:orientation="horizontal"
                android:layout_marginBottom="10dp"
                android:paddingStart="30dp"
                tools:ignore="RtlSymmetry">

                <androidx.appcompat.widget.AppCompatImageView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_vertical"
                    android:src="@drawable/ic_log_out"
                    android:tint="@color/gray_B3BAC2" />

                <TextView
                    style="@style/TextBodyBold.Large"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:paddingVertical="20dp"
                    android:paddingStart="25dp"
                    android:paddingEnd="50dp"
                    android:text="@string/label_logout"
                    android:textColor="@color/gray_B3BAC2" />
            </LinearLayout>
        </com.google.android.material.navigation.NavigationView>
    </androidx.drawerlayout.widget.DrawerLayout>

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private lateinit var mainActivityBinding: ActivityMainBinding
    // ...
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        mainActivityBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        mainActivityBinding.apply {
            lifecycleOwner = this@MainActivity
            llActivityMainLogOut.setOnClickListener { mainViewModel.handleOnLogoutUserPress() }
        }
    }

    private var onScrollListener: RecyclerView.OnScrollListener? = null

    /**
     * Listener for "Logout" Drawer Menu Item that checks if should
     * add a scrollListener (and update bottom margin) or keep the item at the bottom of the view.
     */
    private val navigationViewGlobalLayoutListener = object : OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            // Get the NavigationView RecyclerView List Menu
            val drawerViewList = mainActivityBinding.navigationView.getChildAt(0) as RecyclerView

            if (onScrollListener == null) {
                val scrollExtent = drawerViewList.computeVerticalScrollExtent()
                val scrollRange = drawerViewList.computeVerticalScrollRange()
                // Check if the list has enough height space for all items
                if (scrollExtent < scrollRange) {
                    // Adding NavigationView Bottom padding. Is where the logoutItem goes
                    mainActivityBinding.navigationView.setPadding(
                        0,
                        0,
                        0,
                        resources.getDimensionPixelSize(R.dimen.drawer_layout_footer_padding_bottom)
                    )

                    // The first item of the list is a Header, getting the second menu item height
                    val itemHeight = drawerViewList.getChildAt(1).measuredHeight
                    // The first update will add enough negative bottom margin and will hide the item
                    updateDrawerLogoutItemBottomMargin(scrollExtent, scrollRange, itemHeight)

                    onScrollListener = object : RecyclerView.OnScrollListener() {
                        private var totalScrollY = 0

                        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                            totalScrollY += dy

                            updateDrawerLogoutItemBottomMargin(
                                recyclerView.computeVerticalScrollExtent(),
                                recyclerView.computeVerticalScrollRange(),
                                recyclerView.getChildAt(1).measuredHeight,
                                totalScrollY
                            )
                        }
                    }

                    // Adding scroll listener in order to update the bottom logOut item margin
                    // while the list is being scrolled
                    drawerViewList.addOnScrollListener(onScrollListener!!)
                }
            } else {
                // The listener have been already added and computed, removing
                drawerViewList.viewTreeObserver.removeOnGlobalLayoutListener(this)
            }
        }
    }

    /**
     * @param scrollExtent the current height of the list
     * @param scrollRange the height that needs the list for showing all items
     * @param itemHeight the height of a Menu Item (not header)
     * @param extraScroll scroll offset performed
     */
    private fun updateDrawerLogoutItemBottomMargin(
        scrollExtent: Int,
        scrollRange: Int,
        itemHeight: Int,
        extraScroll: Int = 0
    ) {
        mainActivityBinding.llActivityMainLogOut.updateLayoutParams {
            this as MarginLayoutParams
            bottomMargin = scrollExtent - scrollRange - itemHeight + extraScroll
        }
    }

    override fun onResume() {
        super.onResume()
        mainActivityBinding.navigationView.viewTreeObserver.addOnGlobalLayoutListener(
            navigationViewGlobalLayoutListener
        )
    }

    override fun onPause() {
        super.onPause()
        if (onScrollListener != null) {
            (mainActivityBinding.navigationView.getChildAt(0) as RecyclerView)
                .removeOnScrollListener(onScrollListener!!)
        }
    }

    //...
}

activity_main.xmlNavigationViewandroid:clipChildren="false"android:clipToPadding="false"是必需的参数。

每种情况可能需要不同的底部填充来添加“额外”的抽屉项,在我的情况下,值为:<dimen name="drawer_layout_footer_padding_bottom">70dp</dimen>

当然,不要忘记在onResume方法中注册GlobalLayoutListener,如果已经注册,则在onPause方法中注销scrollListener


0

适用于版本> 23.x.x的滚动页脚

我终于成功实现了我想要的效果,不幸的是,像 Andrea Baccega 描述的在23.x.x以下的版本中仅需抓取对ListView的引用并添加页眉和页脚的方法已经不再可行。但对页眉的操作仍然是可能的:

     <android.support.design.widget.NavigationView
     ..
     app:headerLayout="@layout/item_drawer_footer"
     ..
     />

但目前无法添加页脚。不过,如果您只是想添加页脚,我找到了一个解决方法:只需反转视图,这将在底部添加标题,其行为类似于正常的页脚。只需确保以相反的顺序创建菜单即可。

    // Grab reference to the embedded recycler view
    RecyclerView mRecyclerView = (RecyclerView) navigationView.getChildAt(0);

    // Create a LinearLayoutManager and set it to reversed
    LinearLayoutManager mLayoutManager = new LinearLayoutManager(this);
    mLayoutManager.setReverseLayout(true);

    // Apply layout manager to the recycler view
    mRecyclerView.setLayoutManager(mLayoutManager);

0
如果您正在使用listview,可以尝试以下方法。listview支持添加页脚视图功能,因此您可以使用以下代码将自定义页脚R.layout.drawer_footer视图添加到listview对象中。
View footerView = LayoutInflater.from(getApplicationContext()).inflate(R.layout.drawer_footer, expandableListView, false);
TextView footer = footerView.findViewById(R.id.id_footer_wta_version_information);
expandableListView.addFooterView(footerView);

在我的情况下,我使用可扩展的ListView而不是普通的ListView(普通的ListView也有addFooter函数)。希望这种方式能够帮到你。

-2
将这些代码行放入您的菜单布局中。希望它能解决您的问题:
    <group
        android:id="@+id/grp11"
        android:checkableBehavior="single">

        <item


            android:title="" />
        <item


            android:title="" />
        <item

            android:title="" />
        <item


            android:title="" />
        <item


            android:title="" />
        <item

            android:title="" />

    </group>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接